| /* Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* |
| * Issues |
| * |
| * + Known low-level code kludges/problems |
| * . proxy: an httpd child process validates SCTs from a server only on the |
| * first time the data is received; but it could fail once due to invalid |
| * timestamp, and not be rechecked later after (potentially) time elapses |
| * and the timestamp is now in a valid range |
| * . server: shouldn't have to read file of server SCTs on every handshake |
| * (shared memory or cached file?) |
| * . split mod_ssl_ct.c into more pieces |
| * . research: Is it possible to send an SCT that is outside of the known |
| * valid interval for the log? |
| */ |
| |
| #if defined(WIN32) |
| #define HAVE_SCT_DAEMON_THREAD |
| #else |
| #define HAVE_SCT_DAEMON_CHILD |
| #endif |
| |
| #include <limits.h> |
| |
| #if defined(HAVE_SCT_DAEMON_CHILD) |
| #include <unistd.h> |
| #endif |
| |
| #include "apr_version.h" |
| #if !APR_VERSION_AT_LEAST(1,5,0) |
| #error mod_ssl_ct requires APR 1.5.0 or later! (for apr_escape.h) |
| #endif |
| |
| #include "apr_escape.h" |
| #include "apr_global_mutex.h" |
| #include "apr_signal.h" |
| #include "apr_strings.h" |
| #include "apr_thread_rwlock.h" |
| |
| #include "apr_dbd.h" |
| |
| #include "httpd.h" |
| #include "http_config.h" |
| #include "http_core.h" |
| #include "http_log.h" |
| #include "http_main.h" |
| #include "http_protocol.h" |
| #include "mpm_common.h" |
| #include "util_mutex.h" |
| #include "ap_listen.h" |
| #include "ap_mpm.h" |
| |
| #if AP_NEED_SET_MUTEX_PERMS |
| #include "unixd.h" |
| #endif |
| |
| #include "mod_proxy.h" |
| #include "mod_ssl.h" |
| #include "mod_ssl_openssl.h" |
| |
| #include "ssl_ct_util.h" |
| #include "ssl_ct_sct.h" |
| |
| #include "openssl/x509v3.h" |
| #include "openssl/ocsp.h" |
| |
| #if OPENSSL_VERSION_NUMBER < 0x10002003L |
| #error "mod_ssl_ct requires OpenSSL 1.0.2-beta3 or later" |
| #endif |
| |
| #ifdef WIN32 |
| #define DOTEXE ".exe" |
| #else |
| #define DOTEXE "" |
| #endif |
| |
| #define CLIENT_STATUS_VAR "SSL_CT_CLIENT_STATUS" |
| #define PROXY_STATUS_VAR "SSL_CT_PROXY_STATUS" |
| #define STATUS_VAR_AWARE_VAL "peer-aware" |
| #define STATUS_VAR_UNAWARE_VAL "peer-unaware" |
| |
| #define PROXY_SCT_SOURCES_VAR "SSL_CT_PROXY_SCT_SOURCES" |
| |
| #define DAEMON_NAME "SCT maintenance daemon" |
| #define DAEMON_THREAD_NAME DAEMON_NAME " thread" |
| #define SERVICE_THREAD_NAME "service thread" |
| |
| /** Limit on size of stored SCTs for a certificate (individual SCTs as well |
| * as size of all. |
| */ |
| #define MAX_SCTS_SIZE 10000 |
| |
| /** Limit on size of log URL list for a certificate |
| */ |
| #define MAX_LOGLIST_SIZE 1000 |
| |
| typedef struct ct_server_config { |
| apr_array_header_t *db_log_config; |
| apr_pool_t *db_log_config_pool; |
| apr_array_header_t *static_log_config; |
| apr_array_header_t *server_cert_info; /* ct_server_cert_info */ |
| apr_hash_t *static_cert_sct_dirs; |
| const char *sct_storage; |
| const char *audit_storage; |
| const char *ct_exe; |
| const char *log_config_fname; |
| apr_time_t max_sct_age; |
| int max_sh_sct; |
| #define PROXY_AWARENESS_UNSET -1 |
| #define PROXY_OBLIVIOUS 1 |
| #define PROXY_AWARE 2 /* default */ |
| #define PROXY_REQUIRE 3 |
| int proxy_awareness; |
| } ct_server_config; |
| |
| typedef struct ct_conn_config { |
| int peer_ct_aware; |
| int client_handshake; |
| int proxy_handshake; |
| /* proxy mode only */ |
| cert_chain *certs; |
| int server_cert_has_sct_list; |
| void *cert_sct_list; |
| apr_size_t cert_sct_list_size; |
| int serverhello_has_sct_list; |
| void *serverhello_sct_list; |
| apr_size_t serverhello_sct_list_size; |
| int ocsp_has_sct_list; |
| void *ocsp_sct_list; |
| apr_size_t ocsp_sct_list_size; |
| apr_array_header_t *all_scts; /* array of ct_sct_data */ |
| } ct_conn_config; |
| |
| typedef struct ct_server_cert_info { |
| const char *fingerprint; |
| const char *sct_dir; |
| } ct_server_cert_info; |
| |
| typedef struct ct_sct_data { |
| const void *data; |
| apr_uint16_t len; |
| } ct_sct_data; |
| |
| typedef struct ct_callback_info { |
| server_rec *s; |
| conn_rec *c; |
| ct_conn_config *conncfg; |
| } ct_callback_info; |
| |
| typedef struct ct_cached_server_data { |
| apr_status_t validation_result; |
| } ct_cached_server_data; |
| |
| /* the log configuration in use -- either db_log_config or static_log_config */ |
| static apr_array_header_t *active_log_config; |
| |
| module AP_MODULE_DECLARE_DATA ssl_ct_module; |
| |
| #define SSL_CT_MUTEX_TYPE "ssl-ct-sct-update" |
| |
| static apr_global_mutex_t *ssl_ct_sct_update; |
| |
| static int refresh_all_scts(server_rec *s_main, apr_pool_t *p, |
| apr_array_header_t *log_config); |
| |
| static ct_server_config *copy_ct_server_config(apr_pool_t *p, |
| ct_server_config *base); |
| |
| static apr_thread_t *service_thread; |
| |
| static apr_hash_t *cached_server_data; |
| |
| static const char *audit_fn_perm, *audit_fn_active; |
| static apr_file_t *audit_file; |
| static int audit_file_nonempty; |
| static apr_thread_mutex_t *audit_file_mutex; |
| static apr_thread_mutex_t *cached_server_data_mutex; |
| static apr_thread_rwlock_t *log_config_rwlock; |
| |
| #ifdef HAVE_SCT_DAEMON_CHILD |
| |
| /* The APR other-child API doesn't tell us how the daemon exited |
| * (SIGSEGV vs. exit(1)). The other-child maintenance function |
| * needs to decide whether to restart the daemon after a failure |
| * based on whether or not it exited due to a fatal startup error |
| * or something that happened at steady-state. This exit status |
| * is unlikely to collide with exit signals. |
| */ |
| #define DAEMON_STARTUP_ERROR 254 |
| |
| static int daemon_start(apr_pool_t *p, server_rec *main_server, apr_proc_t *procnew); |
| static server_rec *root_server = NULL; |
| static apr_pool_t *root_pool = NULL; |
| static pid_t daemon_pid; |
| static int daemon_should_exit = 0; |
| |
| #endif /* HAVE_SCT_DAEMON_CHILD */ |
| |
| static apr_pool_t *pdaemon = NULL; |
| |
| #ifdef HAVE_SCT_DAEMON_THREAD |
| static apr_thread_t *daemon_thread; |
| #endif /* HAVE_SCT_DAEMON_THREAD */ |
| |
| static const char *get_cert_fingerprint(apr_pool_t *p, const X509 *x) |
| { |
| const EVP_MD *digest; |
| unsigned char md[EVP_MAX_MD_SIZE]; |
| unsigned int n; |
| digest = EVP_get_digestbyname("sha256"); |
| X509_digest(x, digest, md, &n); |
| |
| return apr_pescape_hex(p, md, n, 0); |
| } |
| |
| /* a server's SCT-related storage on disk: |
| * |
| * <rootdir>/<fingerprint>/servercerts.pem |
| * Concatenation of leaf certificate and any |
| * configured intermediate certificates |
| * |
| * <rootdir>/<fingerprint>/logs |
| * List of log URLs, one per line; this is |
| * used to recognize when a log URL configuration |
| * change makes our current SCT set invalid |
| * |
| * <rootdir>/<fingerprint>/AUTO_hostname_port_uri.sct |
| * SCT for cert with this fingerprint |
| * from this log (could be any number |
| * of these) |
| * |
| * <rootdir>/<fingerprint>/<anything>.sct |
| * (file is optional; could be any number |
| * of these; should not start with "AUTO_") |
| * Note that the administrator should store |
| * statically maintained SCTs in a different |
| * directory for the server certificate (specified |
| * by the CTStaticSCTs directive). A hypothetical |
| * external mechanism for maintaining SCTs following |
| * some other model could store them here for use |
| * by the server. |
| * |
| * <rootdir>/<fingerprint>/collated |
| * one or more SCTs ready to send |
| * (this is all that the web server |
| * processes care about) |
| * |
| * Additionally, the CTStaticSCTs directive specifies a certificate- |
| * specific directory of statically-maintained SCTs to be sent. |
| */ |
| |
| #define SERVERCERTS_BASENAME "servercerts.pem" |
| #define COLLATED_SCTS_BASENAME "collated" |
| #define LOGLIST_BASENAME "logs" |
| #define LOG_SCT_PREFIX "AUTO_" /* to distinguish from admin-created .sct |
| * files |
| */ |
| |
| static apr_status_t collate_scts(server_rec *s, apr_pool_t *p, |
| const char *cert_sct_dir, |
| const char *static_cert_sct_dir, |
| int max_sh_sct) |
| { |
| /* Read the various .sct files and stick them together in a single file */ |
| apr_array_header_t *arr; |
| apr_status_t rv, tmprv; |
| apr_file_t *tmpfile; |
| apr_size_t bytes_written; |
| apr_uint16_t overall_len = 0; |
| char *tmp_collated_fn, *collated_fn; |
| const char *cur_sct_file; |
| const char * const *elts; |
| int i, scts_written = 0, skipped = 0; |
| |
| rv = ctutil_path_join(&collated_fn, cert_sct_dir, COLLATED_SCTS_BASENAME, p, s); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| /* Note: We rebuild the file that combines the SCTs every time this |
| * code runs, even if no individual SCTs are new (or at least |
| * re-fetched). |
| * That allows the admin to see the last processing by looking |
| * at the timestamp. |
| * Rechecking even if no SCTs are new allows SCTs which were not |
| * yet valid originally (just submitted to a log) to be used as |
| * soon as practical. |
| */ |
| tmp_collated_fn = apr_pstrcat(p, collated_fn, ".tmp", NULL); |
| |
| rv = apr_file_open(&tmpfile, tmp_collated_fn, |
| APR_FOPEN_WRITE|APR_FOPEN_CREATE|APR_FOPEN_TRUNCATE |
| |APR_FOPEN_BINARY|APR_FOPEN_BUFFERED, |
| APR_FPROT_OS_DEFAULT, p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, |
| APLOGNO(02683) "can't create %s", tmp_collated_fn); |
| return rv; |
| } |
| |
| /* stick a 0 len (for the list) at the start of the file; |
| * we'll have to patch that later |
| */ |
| rv = ctutil_file_write_uint16(s, tmpfile, overall_len); |
| if (rv != APR_SUCCESS) { |
| apr_file_close(tmpfile); |
| return rv; |
| } |
| |
| arr = NULL; /* Build list from scratch, creating array */ |
| rv = ctutil_read_dir(p, s, cert_sct_dir, "*.sct", &arr); |
| if (rv != APR_SUCCESS) { |
| apr_file_close(tmpfile); |
| return rv; |
| } |
| |
| if (static_cert_sct_dir) { |
| /* Add in any SCTs that the administrator has configured */ |
| rv = ctutil_read_dir(p, s, static_cert_sct_dir, "*.sct", &arr); |
| if (rv != APR_SUCCESS) { |
| apr_file_close(tmpfile); |
| return rv; |
| } |
| } |
| |
| elts = (const char * const *)arr->elts; |
| |
| for (i = 0; i < arr->nelts; i++) { |
| char *scts; |
| apr_size_t scts_size_wide; |
| apr_uint16_t scts_size; |
| sct_fields_t fields; |
| |
| cur_sct_file = elts[i]; |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(03022) |
| "Adding SCT from file %s", cur_sct_file); |
| |
| rv = ctutil_read_file(p, s, cur_sct_file, MAX_SCTS_SIZE, &scts, |
| &scts_size_wide); |
| if (rv != APR_SUCCESS) { |
| break; |
| } |
| ap_assert(scts_size_wide <= USHRT_MAX); |
| scts_size = (apr_uint16_t)scts_size_wide; |
| |
| rv = sct_parse(cur_sct_file, |
| s, (const unsigned char *)scts, scts_size, NULL, &fields); |
| if (rv != APR_SUCCESS) { |
| sct_release(&fields); |
| break; |
| } |
| |
| /* If the SCT has a timestamp in the future, it may have just been |
| * created by the log. |
| */ |
| if (fields.time > apr_time_now()) { |
| ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, |
| APLOGNO(02684) "SCT in file %s has timestamp in future " |
| "(%s), skipping", |
| cur_sct_file, fields.timestr); |
| sct_release(&fields); |
| continue; |
| } |
| |
| sct_release(&fields); |
| |
| /* Only now do we know that the SCT is valid to send, so |
| * see if it has to be skipped by configured limit. |
| */ |
| if (scts_written >= max_sh_sct) { |
| skipped++; |
| continue; |
| } |
| |
| overall_len += scts_size + 2; /* include size header */ |
| |
| rv = ctutil_file_write_uint16(s, tmpfile, (apr_uint16_t)scts_size); |
| if (rv != APR_SUCCESS) { |
| break; |
| } |
| |
| rv = apr_file_write_full(tmpfile, scts, scts_size, &bytes_written); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, |
| APLOGNO(02685) "can't write %hu bytes to %s", |
| scts_size, tmp_collated_fn); |
| break; |
| } |
| |
| scts_written++; |
| } |
| |
| if (rv == APR_SUCCESS && skipped) { |
| ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, |
| APLOGNO(02686) "SCTs sent in ServerHello are limited to " |
| "%d by CTServerHelloSCTLimit (ignoring %d)", |
| max_sh_sct, |
| skipped); |
| } |
| |
| if (rv == APR_SUCCESS) { |
| apr_off_t offset = 0; |
| |
| rv = apr_file_seek(tmpfile, APR_SET, &offset); |
| if (rv == APR_SUCCESS) { |
| rv = ctutil_file_write_uint16(s, tmpfile, overall_len); |
| } |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, |
| APLOGNO(02687) "could not write the SCT list length " |
| "at the start of the file"); |
| } |
| } |
| |
| tmprv = apr_file_close(tmpfile); |
| if (tmprv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, tmprv, s, |
| APLOGNO(02688) "error flushing and closing %s", |
| tmp_collated_fn); |
| if (rv == APR_SUCCESS) { |
| rv = tmprv; |
| } |
| } |
| |
| if (rv == APR_SUCCESS && scts_written) { |
| int replacing = ctutil_file_exists(p, collated_fn); |
| |
| if (replacing) { |
| if ((rv = apr_global_mutex_lock(ssl_ct_sct_update)) != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, |
| APLOGNO(02689) "global mutex lock failed"); |
| return rv; |
| } |
| apr_file_remove(collated_fn, p); |
| } |
| rv = apr_file_rename(tmp_collated_fn, collated_fn, p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, |
| APLOGNO(02690) "couldn't rename %s to %s, no SCTs to " |
| "send for now", |
| tmp_collated_fn, collated_fn); |
| } |
| if (replacing) { |
| if ((tmprv = apr_global_mutex_unlock(ssl_ct_sct_update)) != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, tmprv, s, |
| APLOGNO(02691) "global mutex unlock failed"); |
| if (rv == APR_SUCCESS) { |
| rv = tmprv; |
| } |
| } |
| } |
| } |
| |
| return rv; |
| } |
| |
| static const char *url_to_fn(apr_pool_t *p, const apr_uri_t *log_url) |
| { |
| char *fn = apr_psprintf(p, LOG_SCT_PREFIX "%s_%s_%s.sct", |
| log_url->hostname, log_url->port_str, log_url->path); |
| char *ch; |
| |
| ch = fn; |
| while (*ch) { |
| switch(*ch) { |
| /* chars that shouldn't be used in a filename */ |
| case ':': |
| case '/': |
| case '\\': |
| case '*': |
| case '?': |
| case '<': |
| case '>': |
| case '"': |
| case '|': |
| *ch = '-'; |
| } |
| ++ch; |
| } |
| return fn; |
| } |
| |
| static apr_status_t submission(server_rec *s, apr_pool_t *p, const char *ct_exe, |
| const ct_log_config *log_cfg, |
| const char *cert_file, const char *sct_fn) |
| { |
| apr_status_t rv; |
| const char *args[8]; |
| int i; |
| |
| i = 0; |
| args[i++] = ct_exe; |
| args[i++] = apr_pstrcat(p, "--ct_server=", log_cfg->url, NULL); |
| args[i++] = "--logtostderr=true"; |
| args[i++] = apr_pstrcat(p, "--ct_server_submission=", cert_file, NULL); |
| args[i++] = apr_pstrcat(p, "--ct_server_response_out=", sct_fn, NULL); |
| args[i++] = apr_pstrcat(p, "--ct_server_public_key=", log_cfg->public_key_pem, NULL); |
| args[i++] = "upload"; |
| args[i++] = NULL; |
| ap_assert(i == sizeof args / sizeof args[0]); |
| |
| rv = ctutil_run_to_log(p, s, args, "log client"); |
| |
| return rv; |
| } |
| |
| static apr_status_t fetch_sct(server_rec *s, apr_pool_t *p, |
| const char *cert_file, |
| const char *cert_sct_dir, |
| const ct_log_config *log_cfg, |
| const char *ct_exe, apr_time_t max_sct_age) |
| { |
| apr_status_t rv; |
| char *sct_fn; |
| apr_finfo_t finfo; |
| const char *log_url_basename; |
| |
| log_url_basename = url_to_fn(p, &log_cfg->uri); |
| |
| rv = ctutil_path_join(&sct_fn, cert_sct_dir, log_url_basename, p, s); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| rv = apr_stat(&finfo, sct_fn, APR_FINFO_MTIME, p); |
| if (rv == APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(03023) |
| "Found SCT for %s in %s", cert_file, sct_fn); |
| |
| if (finfo.mtime + max_sct_age < apr_time_now()) { |
| ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(02692) |
| "SCT for %s is older than %d seconds, must refresh", |
| cert_file, (int)(apr_time_sec(max_sct_age))); |
| } |
| else { |
| return APR_SUCCESS; |
| } |
| } |
| else { |
| ap_log_error(APLOG_MARK, APLOG_INFO, |
| /* no need to print error string for file-not-found err */ |
| APR_STATUS_IS_ENOENT(rv) ? 0 : rv, |
| s, APLOGNO(02693) |
| "Did not find SCT for %s in %s, must fetch", |
| cert_file, sct_fn); |
| } |
| |
| rv = submission(s, p, ct_exe, log_cfg, cert_file, sct_fn); |
| |
| return rv; |
| } |
| |
| static apr_status_t record_log_urls(server_rec *s, apr_pool_t *p, |
| const char *listfile, apr_array_header_t *log_config) |
| { |
| apr_file_t *f; |
| apr_status_t rv, tmprv; |
| ct_log_config **config_elts; |
| int i; |
| |
| rv = apr_file_open(&f, listfile, |
| APR_FOPEN_WRITE|APR_FOPEN_CREATE|APR_FOPEN_TRUNCATE |
| |APR_FOPEN_BUFFERED, |
| APR_FPROT_OS_DEFAULT, p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, |
| APLOGNO(02694) "can't create %s", listfile); |
| return rv; |
| } |
| |
| config_elts = (ct_log_config **)log_config->elts; |
| |
| for (i = 0; i < log_config->nelts; i++) { |
| if (!log_configured_for_fetching_sct(config_elts[i])) { |
| continue; |
| } |
| if (!log_valid_for_sent_sct(config_elts[i])) { |
| continue; |
| } |
| rv = apr_file_puts(config_elts[i]->uri_str, f); |
| if (rv == APR_SUCCESS) { |
| rv = apr_file_puts("\n", f); |
| } |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, |
| APLOGNO(02695) "error writing to %s", listfile); |
| break; |
| } |
| } |
| |
| tmprv = apr_file_close(f); |
| if (tmprv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, tmprv, s, |
| APLOGNO(02696) "error flushing and closing %s", listfile); |
| if (rv == APR_SUCCESS) { |
| rv = tmprv; |
| } |
| } |
| |
| return rv; |
| } |
| |
| static int uri_in_config(const char *needle, const apr_array_header_t *haystack) |
| { |
| ct_log_config **elts; |
| int i; |
| |
| elts = (ct_log_config **)haystack->elts; |
| for (i = 0; i < haystack->nelts; i++) { |
| if (!log_configured_for_fetching_sct(elts[i])) { |
| continue; |
| } |
| if (!log_valid_for_sent_sct(elts[i])) { |
| continue; |
| } |
| if (!strcmp(needle, elts[i]->uri_str)) { |
| return 1; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static apr_status_t update_log_list_for_cert(server_rec *s, apr_pool_t *p, |
| const char *cert_sct_dir, |
| apr_array_header_t *log_config) |
| { |
| apr_array_header_t *old_urls; |
| apr_size_t contents_size; |
| apr_status_t rv; |
| char *contents, *listfile; |
| |
| /* The set of logs can change, and we need to remove SCTs retrieved |
| * from logs that we no longer trust. To track changes we'll use a |
| * file in the directory for the server certificate. |
| * |
| * (When can the set change? Right now they can only change at [re]start, |
| * but in the future we should be able to find the set of trusted logs |
| * dynamically.) |
| */ |
| |
| rv = ctutil_path_join(&listfile, cert_sct_dir, LOGLIST_BASENAME, p, s); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| if (ctutil_file_exists(p, listfile)) { |
| char **elts; |
| int i; |
| |
| rv = ctutil_read_file(p, s, listfile, MAX_LOGLIST_SIZE, &contents, &contents_size); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| ctutil_buffer_to_array(p, contents, contents_size, &old_urls); |
| |
| elts = (char **)old_urls->elts; |
| for (i = 0; i < old_urls->nelts; i++) { |
| if (!uri_in_config(elts[i], log_config)) { |
| char *sct_for_log; |
| int exists; |
| apr_uri_t uri; |
| |
| rv = apr_uri_parse(p, elts[i], &uri); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, |
| APLOGNO(02697) "unparsable log URL %s in file " |
| "%s - ignoring", |
| elts[i], listfile); |
| /* some garbage in the file? can't map to an auto-maintained SCT, |
| * so just skip it |
| */ |
| continue; |
| } |
| |
| rv = ctutil_path_join(&sct_for_log, cert_sct_dir, url_to_fn(p, &uri), p, s); |
| ap_assert(rv == APR_SUCCESS); |
| exists = ctutil_file_exists(p, sct_for_log); |
| |
| ap_log_error(APLOG_MARK, |
| exists ? APLOG_NOTICE : APLOG_DEBUG, 0, s, |
| APLOGNO(02698) "Log %s is no longer enabled%s", |
| elts[i], |
| exists ? ", removing SCT" : ", no SCT was present"); |
| |
| if (exists) { |
| rv = apr_file_remove(sct_for_log, p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, |
| APLOGNO(02699) "can't remove SCT %s from " |
| "previously trusted log %s", |
| sct_for_log, elts[i]); |
| return rv; |
| } |
| } |
| } |
| } |
| } |
| else { |
| /* can't tell what was trusted before; just remove everything |
| * that was created automatically |
| */ |
| apr_array_header_t *arr; |
| const char * const *elts; |
| int i; |
| |
| ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, |
| APLOGNO(02700) "List of previous logs doesn't exist (%s), " |
| "removing previously obtained SCTs", |
| listfile); |
| |
| arr = NULL; /* Build list from scratch, creating array */ |
| rv = ctutil_read_dir(p, s, cert_sct_dir, LOG_SCT_PREFIX "*.sct", &arr); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| elts = (const char * const *)arr->elts; |
| for (i = 0; i < arr->nelts; i++) { |
| const char *cur_sct_file = elts[i]; |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(03024) |
| "Removing %s", cur_sct_file); |
| |
| rv = apr_file_remove(cur_sct_file, p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, |
| APLOGNO(02701) "can't remove %s", cur_sct_file); |
| } |
| } |
| } |
| |
| if (rv == APR_SUCCESS) { |
| rv = record_log_urls(s, p, listfile, log_config); |
| } |
| |
| return rv; |
| } |
| |
| static apr_status_t refresh_scts_for_cert(server_rec *s, apr_pool_t *p, |
| const char *cert_sct_dir, |
| const char *static_cert_sct_dir, |
| apr_array_header_t *log_config, |
| const char *ct_exe, |
| apr_time_t max_sct_age, |
| int max_sh_sct) |
| { |
| apr_status_t rv; |
| ct_log_config **config_elts; |
| char *cert_fn; |
| int i; |
| |
| rv = ctutil_path_join(&cert_fn, cert_sct_dir, SERVERCERTS_BASENAME, p, s); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| config_elts = (ct_log_config **)log_config->elts; |
| |
| if (ct_exe) { |
| rv = update_log_list_for_cert(s, p, cert_sct_dir, log_config); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| for (i = 0; i < log_config->nelts; i++) { |
| if (!log_configured_for_fetching_sct(config_elts[i])) { |
| continue; |
| } |
| if (!log_valid_for_sent_sct(config_elts[i])) { |
| continue; |
| } |
| rv = fetch_sct(s, p, cert_fn, |
| cert_sct_dir, |
| config_elts[i], |
| ct_exe, |
| max_sct_age); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| } |
| } |
| else { |
| /* Log client tool (from certificate-transparency open source project) |
| * not configured; we can only use admin-managed SCTs |
| */ |
| } |
| |
| rv = collate_scts(s, p, cert_sct_dir, static_cert_sct_dir, max_sh_sct); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| return rv; |
| } |
| |
| static void * APR_THREAD_FUNC run_service_thread(apr_thread_t *me, void *data) |
| { |
| server_rec *s = data; |
| ct_server_config *sconf = ap_get_module_config(s->module_config, |
| &ssl_ct_module); |
| int mpmq_s; |
| apr_status_t rv; |
| int count = 0; |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(03241) |
| SERVICE_THREAD_NAME " started"); |
| |
| while (1) { |
| if ((rv = ap_mpm_query(AP_MPMQ_MPM_STATE, &mpmq_s)) != APR_SUCCESS) { |
| break; |
| } |
| if (mpmq_s == AP_MPMQ_STOPPING) { |
| break; |
| } |
| apr_sleep(apr_time_from_sec(1)); |
| if (++count >= 30) { |
| count = 0; |
| if (sconf->db_log_config) { |
| /* Reload log config DB */ |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(03242) |
| SERVICE_THREAD_NAME " - reloading config"); |
| ap_assert(apr_thread_rwlock_wrlock(log_config_rwlock) == 0); |
| active_log_config = NULL; |
| apr_pool_clear(sconf->db_log_config_pool); |
| sconf->db_log_config = |
| apr_array_make(sconf->db_log_config_pool, 2, |
| sizeof(ct_log_config *)); |
| rv = read_config_db(sconf->db_log_config_pool, |
| s, sconf->log_config_fname, |
| sconf->db_log_config); |
| ap_assert(apr_thread_rwlock_unlock(log_config_rwlock) == 0); |
| if (rv != APR_SUCCESS) { |
| /* specific issue already logged */ |
| ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s, |
| APLOGNO(02702) SERVICE_THREAD_NAME " - no " |
| "active configuration until " |
| "log config DB is corrected"); |
| } |
| else { |
| active_log_config = sconf->db_log_config; |
| } |
| } |
| } |
| } |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, APLOGNO(03243) |
| SERVICE_THREAD_NAME " exiting"); |
| |
| apr_thread_exit(me, APR_SUCCESS); |
| return NULL; |
| } |
| |
| static apr_status_t wait_for_thread(void *data) |
| { |
| apr_thread_t *thd = data; |
| apr_status_t retval; |
| |
| apr_thread_join(&retval, thd); |
| return APR_SUCCESS; |
| } |
| |
| static void sct_daemon_cycle(ct_server_config *sconf, server_rec *s_main, |
| apr_pool_t *ptemp, const char *daemon_name) |
| { |
| apr_status_t rv; |
| |
| if (sconf->db_log_config) { /* not using static config */ |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s_main, APLOGNO(03025) |
| "%s - reloading config", daemon_name); |
| apr_pool_clear(sconf->db_log_config_pool); |
| active_log_config = NULL; |
| sconf->db_log_config = |
| apr_array_make(sconf->db_log_config_pool, 2, |
| sizeof(ct_log_config *)); |
| rv = read_config_db(sconf->db_log_config_pool, |
| s_main, sconf->log_config_fname, |
| sconf->db_log_config); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s_main, |
| APLOGNO(02703) "%s - no active configuration until " |
| "log config DB is corrected", daemon_name); |
| return; |
| } |
| active_log_config = sconf->db_log_config; |
| } |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s_main, APLOGNO(03026) |
| "%s - refreshing SCTs as needed", daemon_name); |
| rv = refresh_all_scts(s_main, ptemp, active_log_config); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s_main, |
| APLOGNO(02704) "%s - SCT refresh failed; will try again " |
| "later", |
| daemon_name); |
| } |
| } |
| |
| #ifdef HAVE_SCT_DAEMON_CHILD |
| |
| static void daemon_signal_handler(int sig) |
| { |
| if (sig == SIGHUP) { |
| ++daemon_should_exit; |
| } |
| } |
| |
| #if APR_HAS_OTHER_CHILD |
| static void daemon_maint(int reason, void *data, apr_wait_t status) |
| { |
| apr_proc_t *proc = data; |
| int mpm_state; |
| int stopping; |
| |
| switch (reason) { |
| case APR_OC_REASON_DEATH: |
| apr_proc_other_child_unregister(data); |
| /* If apache is not terminating or restarting, |
| * restart the daemon |
| */ |
| stopping = 1; /* if MPM doesn't support query, |
| * assume we shouldn't restart daemon |
| */ |
| if (ap_mpm_query(AP_MPMQ_MPM_STATE, &mpm_state) == APR_SUCCESS && |
| mpm_state != AP_MPMQ_STOPPING) { |
| stopping = 0; |
| } |
| if (!stopping) { |
| if (status == DAEMON_STARTUP_ERROR) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, 0, ap_server_conf, APLOGNO(02634) |
| DAEMON_NAME " failed to initialize"); |
| } |
| else { |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, ap_server_conf, APLOGNO(02635) |
| DAEMON_NAME " process died, restarting"); |
| daemon_start(root_pool, root_server, proc); |
| } |
| } |
| break; |
| case APR_OC_REASON_RESTART: |
| /* don't do anything; server is stopping or restarting */ |
| apr_proc_other_child_unregister(data); |
| break; |
| case APR_OC_REASON_LOST: |
| /* Restart the child cgid daemon process */ |
| apr_proc_other_child_unregister(data); |
| daemon_start(root_pool, root_server, proc); |
| break; |
| case APR_OC_REASON_UNREGISTER: |
| /* we get here when pcgi is cleaned up; pcgi gets cleaned |
| * up when pconf gets cleaned up |
| */ |
| kill(proc->pid, SIGHUP); /* send signal to daemon telling it to die */ |
| break; |
| } |
| } |
| #endif |
| |
| static int sct_daemon(server_rec *s_main) |
| { |
| apr_status_t rv; |
| apr_pool_t *ptemp; |
| ct_server_config *sconf = ap_get_module_config(s_main->module_config, |
| &ssl_ct_module); |
| int rc; |
| |
| /* Ignoring SIGCHLD results in errno ECHILD returned from apr_proc_wait(). |
| * apr_signal(SIGCHLD, SIG_IGN); |
| */ |
| apr_signal(SIGHUP, daemon_signal_handler); |
| |
| /* Close our copy of the listening sockets */ |
| ap_close_listeners(); |
| |
| rv = apr_global_mutex_child_init(&ssl_ct_sct_update, |
| apr_global_mutex_lockfile(ssl_ct_sct_update), pdaemon); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, root_server, |
| APLOGNO(02705) "could not initialize " SSL_CT_MUTEX_TYPE |
| " mutex in " DAEMON_NAME); |
| return DAEMON_STARTUP_ERROR; |
| } |
| |
| if (!geteuid()) { |
| /* Fix up permissions of the directories written to by the daemon |
| */ |
| int i; |
| apr_array_header_t *subdirs = apr_array_make(pdaemon, 5, sizeof(char *)); |
| |
| *(const char **)apr_array_push(subdirs) = sconf->sct_storage; |
| if (sconf->audit_storage) { |
| *(const char **)apr_array_push(subdirs) = sconf->audit_storage; |
| } |
| |
| rv = ctutil_read_dir(pdaemon, root_server, sconf->sct_storage, "*", |
| &subdirs); |
| if (rv == APR_SUCCESS && subdirs->nelts > 0) { |
| const char * const *elts = (const char * const *)subdirs->elts; |
| |
| for (i = 0; i < subdirs->nelts; i++) { |
| if (elts[i] && chown(elts[i], ap_unixd_config.user_id, |
| ap_unixd_config.group_id) < 0) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, errno, root_server, |
| APLOGNO(02706) "Couldn't change owner or group of " |
| "directory %s", |
| elts[i]); |
| return errno; |
| } |
| } |
| } |
| else { |
| ap_log_error(APLOG_MARK, APLOG_WARNING, rv, root_server, |
| APLOGNO(02707) "Did not read any entries from %s (no " |
| "server certificate?)", |
| sconf->sct_storage); |
| } |
| } |
| |
| /* if running as root, switch to configured user/group */ |
| if ((rc = ap_run_drop_privileges(pdaemon, ap_server_conf)) != 0) { |
| return rc; |
| } |
| |
| /* ptemp - temporary pool for refresh cycles */ |
| apr_pool_create(&ptemp, pdaemon); |
| apr_pool_tag(ptemp, "sct_daemon_refresh"); |
| |
| while (!daemon_should_exit) { |
| sct_daemon_cycle(sconf, s_main, ptemp, DAEMON_NAME); |
| apr_sleep(apr_time_from_sec(30)); /* SIGHUP at restart/stop will break out */ |
| } |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s_main, APLOGNO(03244) |
| DAEMON_NAME " - exiting"); |
| |
| return 0; |
| } |
| |
| static int daemon_start(apr_pool_t *p, server_rec *main_server, |
| apr_proc_t *procnew) |
| { |
| daemon_should_exit = 0; /* clear setting from previous generation */ |
| if ((daemon_pid = fork()) < 0) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, errno, main_server, |
| APLOGNO(02708) "Couldn't create " DAEMON_NAME " process"); |
| return DECLINED; |
| } |
| else if (daemon_pid == 0) { |
| if (pdaemon == NULL) { |
| apr_pool_create(&pdaemon, p); |
| apr_pool_tag(pdaemon, "sct_daemon"); |
| } |
| exit(sct_daemon(main_server) > 0 ? DAEMON_STARTUP_ERROR : -1); |
| } |
| procnew->pid = daemon_pid; |
| procnew->err = procnew->in = procnew->out = NULL; |
| apr_pool_note_subprocess(p, procnew, APR_KILL_AFTER_TIMEOUT); |
| #if APR_HAS_OTHER_CHILD |
| apr_proc_other_child_register(procnew, daemon_maint, procnew, NULL, p); |
| #endif |
| return OK; |
| } |
| #endif /* HAVE_SCT_DAEMON_CHILD */ |
| |
| #ifdef HAVE_SCT_DAEMON_THREAD |
| static void *sct_daemon_thread(apr_thread_t *me, void *data) |
| { |
| server_rec *s = data; |
| ct_server_config *sconf = ap_get_module_config(s->module_config, |
| &ssl_ct_module); |
| int mpmq_s; |
| apr_pool_t *ptemp; |
| apr_status_t rv; |
| int count = 0; |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(03245) |
| DAEMON_THREAD_NAME " started"); |
| |
| /* ptemp - temporary pool for refresh cycles */ |
| apr_pool_create(&ptemp, pdaemon); |
| apr_pool_tag(ptemp, "sct_daemon_thread"); |
| |
| while (1) { |
| if ((rv = ap_mpm_query(AP_MPMQ_MPM_STATE, &mpmq_s)) != APR_SUCCESS) { |
| break; |
| } |
| if (mpmq_s == AP_MPMQ_STOPPING) { |
| break; |
| } |
| apr_sleep(apr_time_from_sec(1)); |
| if (++count >= 30) { |
| count = 0; |
| sct_daemon_cycle(sconf, s, ptemp, DAEMON_THREAD_NAME); |
| } |
| } |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(03246) |
| DAEMON_THREAD_NAME " - exiting"); |
| |
| apr_thread_exit(me, APR_SUCCESS); |
| return NULL; |
| } |
| |
| static int daemon_thread_start(apr_pool_t *pconf, server_rec *s_main) |
| { |
| apr_status_t rv; |
| |
| apr_pool_create(&pdaemon, pconf); |
| apr_pool_tag(pdaemon, "sct_daemon"); |
| rv = ap_thread_create(&daemon_thread, NULL, sct_daemon_thread, s_main, |
| pconf); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s_main, |
| APLOGNO(02709) "could not create " DAEMON_THREAD_NAME |
| " in parent"); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| |
| apr_pool_pre_cleanup_register(pconf, daemon_thread, wait_for_thread); |
| |
| return OK; |
| } |
| #endif /* HAVE_SCT_DAEMON_THREAD */ |
| |
| static apr_status_t ssl_ct_mutex_remove(void *data) |
| { |
| apr_global_mutex_destroy(ssl_ct_sct_update); |
| ssl_ct_sct_update = NULL; |
| return APR_SUCCESS; |
| } |
| |
| static int refresh_all_scts(server_rec *s_main, apr_pool_t *p, |
| apr_array_header_t *log_config) |
| { |
| apr_hash_t *already_processed; |
| apr_status_t rv = APR_SUCCESS; |
| server_rec *s; |
| |
| already_processed = apr_hash_make(p); |
| |
| s = s_main; |
| while (s) { |
| ct_server_config *sconf = ap_get_module_config(s->module_config, |
| &ssl_ct_module); |
| int i; |
| const ct_server_cert_info *cert_info_elts; |
| |
| if (sconf && sconf->server_cert_info) { |
| cert_info_elts = |
| (const ct_server_cert_info *)sconf->server_cert_info->elts; |
| for (i = 0; i < sconf->server_cert_info->nelts; i++) { |
| /* we may have already processed this cert for another |
| * server_rec |
| */ |
| if (!apr_hash_get(already_processed, cert_info_elts[i].sct_dir, |
| APR_HASH_KEY_STRING)) { |
| const char *static_cert_sct_dir = |
| apr_hash_get(sconf->static_cert_sct_dirs, |
| cert_info_elts[i].fingerprint, |
| APR_HASH_KEY_STRING); |
| |
| apr_hash_set(already_processed, cert_info_elts[i].sct_dir, |
| APR_HASH_KEY_STRING, "done"); |
| rv = refresh_scts_for_cert(s_main, p, |
| cert_info_elts[i].sct_dir, |
| static_cert_sct_dir, |
| log_config, |
| sconf->ct_exe, |
| sconf->max_sct_age, |
| sconf->max_sh_sct); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| } |
| } |
| } |
| |
| s = s->next; |
| } |
| |
| return rv; |
| } |
| |
| static int num_server_certs(server_rec *s_main) |
| { |
| int num = 0; |
| server_rec *s; |
| |
| s = s_main; |
| while (s) { |
| ct_server_config *sconf = ap_get_module_config(s->module_config, |
| &ssl_ct_module); |
| |
| if (sconf && sconf->server_cert_info) { |
| num += sconf->server_cert_info->nelts; |
| } |
| s = s->next; |
| } |
| |
| return num; |
| } |
| |
| static int ssl_ct_post_config(apr_pool_t *pconf, apr_pool_t *plog, |
| apr_pool_t *ptemp, server_rec *s_main) |
| { |
| ct_server_config *sconf = ap_get_module_config(s_main->module_config, |
| &ssl_ct_module); |
| apr_status_t rv; |
| #ifdef HAVE_SCT_DAEMON_CHILD |
| apr_proc_t *procnew = NULL; |
| const char *userdata_key = "sct_daemon_init"; |
| |
| root_server = s_main; |
| root_pool = pconf; |
| |
| procnew = ap_retained_data_get(userdata_key); |
| if (!procnew) { |
| procnew = ap_retained_data_create(userdata_key, sizeof(*procnew)); |
| procnew->pid = -1; |
| procnew->err = procnew->in = procnew->out = NULL; |
| } |
| #endif /* HAVE_SCT_DAEMON_CHILD */ |
| |
| if (num_server_certs(s_main) == 0) { |
| /* Theoretically this module could operate in a proxy-only |
| * configuration, where httpd does not act as a TLS server but proxy is |
| * configured as a TLS client. That isn't currently implemented. |
| */ |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s_main, |
| APLOGNO(02710) "No server certificates were found."); |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s_main, |
| APLOGNO(02711) "mod_ssl_ct only supports configurations " |
| "with a TLS server."); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| |
| rv = ap_global_mutex_create(&ssl_ct_sct_update, NULL, |
| SSL_CT_MUTEX_TYPE, NULL, s_main, pconf, 0); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s_main, |
| APLOGNO(02712) "could not create global mutex"); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| |
| apr_pool_cleanup_register(pconf, (void *)s_main, ssl_ct_mutex_remove, |
| apr_pool_cleanup_null); |
| |
| if (sconf->log_config_fname) { |
| if (!sconf->db_log_config) { |
| /* log config db in separate pool that can be cleared */ |
| apr_pool_create(&sconf->db_log_config_pool, pconf); |
| apr_pool_tag(sconf->db_log_config_pool, "sct_db_log_config"); |
| sconf->db_log_config = |
| apr_array_make(sconf->db_log_config_pool, 2, |
| sizeof(ct_log_config *)); |
| } |
| rv = read_config_db(sconf->db_log_config_pool, |
| s_main, sconf->log_config_fname, |
| sconf->db_log_config); |
| if (rv != APR_SUCCESS) { |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| } |
| |
| if (sconf->static_log_config && sconf->db_log_config) { |
| if (sconf->static_log_config->nelts > 0 |
| && sconf->db_log_config->nelts > 0) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s_main, |
| APLOGNO(02713) "Either the static log configuration or " |
| "the db log configuration must be empty"); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| } |
| |
| if (sconf->static_log_config && sconf->static_log_config->nelts > 0) { |
| active_log_config = sconf->static_log_config; |
| } |
| else if (sconf->db_log_config && sconf->db_log_config->nelts > 0) { |
| active_log_config = sconf->db_log_config; |
| } |
| else { |
| ap_log_error(APLOG_MARK, APLOG_INFO, 0, s_main, |
| APLOGNO(02714) "No log URLs were configured; only admin-" |
| "managed SCTs can be sent"); |
| /* if a db is configured, it could be updated later */ |
| if (!sconf->db_log_config) { /* no DB configured, need permanently |
| * empty array */ |
| active_log_config = apr_array_make(pconf, 1, |
| sizeof(ct_log_config *)); |
| } |
| } |
| |
| /* Ensure that we already have, or can fetch, fresh SCTs for each |
| * certificate. If so, start the daemon to maintain these and let |
| * startup continue. (Otherwise abort startup.) |
| * |
| * Except when we start up as root. We don't want to run external |
| * certificate-transparency tools as root, and we don't want to have |
| * to fix up the permissions of everything we created so that the |
| * SCT maintenance daemon can continue to maintain the SCTs as the |
| * configured User/Group. |
| */ |
| |
| #if AP_NEED_SET_MUTEX_PERMS /* Unix :) */ |
| if (!geteuid()) { /* root */ |
| ap_log_error(APLOG_MARK, APLOG_INFO, 0, s_main, |
| APLOGNO(02715) "SCTs will be fetched from configured logs " |
| "as needed and may not be available immediately"); |
| } |
| else { |
| #endif |
| rv = refresh_all_scts(s_main, pconf, active_log_config); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s_main, |
| APLOGNO(02716) "refresh_all_scts() failed"); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| #if AP_NEED_SET_MUTEX_PERMS |
| } |
| #endif |
| |
| #ifdef HAVE_SCT_DAEMON_CHILD |
| if (ap_state_query(AP_SQ_MAIN_STATE) != AP_SQ_MS_CREATE_PRE_CONFIG) { |
| int ret = daemon_start(pconf, s_main, procnew); |
| if (ret != OK) { |
| return ret; |
| } |
| } |
| #endif /* HAVE_SCT_DAEMON_CHILD */ |
| |
| #ifdef HAVE_SCT_DAEMON_THREAD |
| /* WIN32-ism: ensure this is the parent by checking AP_PARENT_PID, |
| * which is only set in WinNT children. |
| */ |
| if (ap_state_query(AP_SQ_MAIN_STATE) != AP_SQ_MS_CREATE_PRE_CONFIG |
| && !getenv("AP_PARENT_PID")) { |
| int ret = daemon_thread_start(pconf, s_main); |
| if (ret != OK) { |
| return ret; |
| } |
| } |
| #endif /* HAVE_SCT_DAEMON_THREAD */ |
| |
| return OK; |
| } |
| |
| static int ssl_ct_check_config(apr_pool_t *pconf, apr_pool_t *plog, |
| apr_pool_t *ptemp, server_rec *s_main) |
| { |
| ct_server_config *sconf = ap_get_module_config(s_main->module_config, |
| &ssl_ct_module); |
| |
| if (!sconf->sct_storage) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s_main, |
| APLOGNO(02717) "Directive CTSCTStorage is required"); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| |
| if (!sconf->audit_storage) { |
| /* umm, hard to tell if needed... must have server with |
| * SSL proxy enabled and server-specific-sconf->proxy_awareness |
| * != PROXY_OBLIVIOUS... |
| */ |
| ap_log_error(APLOG_MARK, APLOG_INFO, 0, s_main, |
| APLOGNO(02718) "Directive CTAuditStorage isn't set; proxy " |
| "will not save data for off-line audit"); |
| } |
| |
| if (!sconf->ct_exe) { |
| ap_log_error(APLOG_MARK, APLOG_INFO, 0, s_main, |
| APLOGNO(02719) "Directive CTLogClient isn't set; server " |
| "certificates can't be submitted to configured logs; " |
| "only admin-managed SCTs can be provided to clients"); |
| } |
| |
| if (sconf->log_config_fname) { |
| const char *msg = NULL; |
| if (!log_config_readable(pconf, sconf->log_config_fname, &msg)) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s_main, |
| APLOGNO(02720) "Log config file %s cannot be read", |
| sconf->log_config_fname); |
| if (msg) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s_main, APLOGNO(03027) |
| "%s", msg); |
| } |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| } |
| |
| return OK; |
| } |
| |
| static apr_status_t read_scts(apr_pool_t *p, const char *fingerprint, |
| const char *sct_dir, |
| server_rec *s, |
| char **scts, apr_size_t *scts_len) |
| { |
| apr_status_t rv, tmprv; |
| char *cert_dir, *sct_fn; |
| |
| rv = ctutil_path_join(&cert_dir, sct_dir, fingerprint, p, s); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| rv = ctutil_path_join(&sct_fn, cert_dir, COLLATED_SCTS_BASENAME, p, s); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| if ((rv = apr_global_mutex_lock(ssl_ct_sct_update)) != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, |
| APLOGNO(02721) "global mutex lock failed"); |
| return rv; |
| } |
| |
| rv = ctutil_read_file(p, s, sct_fn, MAX_SCTS_SIZE, scts, scts_len); |
| |
| if ((tmprv = apr_global_mutex_unlock(ssl_ct_sct_update)) != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, tmprv, s, |
| APLOGNO(02722) "global mutex unlock failed"); |
| } |
| |
| return rv; |
| } |
| |
| static void look_for_server_certs(server_rec *s, SSL_CTX *ctx, const char *sct_dir) |
| { |
| ct_server_config *sconf = ap_get_module_config(s->module_config, |
| &ssl_ct_module); |
| apr_pool_t *p = s->process->pool; |
| apr_status_t rv; |
| FILE *concat; |
| X509 *x; |
| STACK_OF(X509) *chain; |
| int i, rc; |
| char *cert_sct_dir, *servercerts_pem; |
| const char *fingerprint; |
| ct_server_cert_info *cert_info; |
| |
| sconf->server_cert_info = apr_array_make(p, 2, sizeof(ct_server_cert_info)); |
| |
| rc = SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_FIRST); |
| while (rc) { |
| x = SSL_CTX_get0_certificate(ctx); /* UNDOC (mentioned in ssl.pod) */ |
| if (x) { |
| fingerprint = get_cert_fingerprint(s->process->pool, x); |
| rv = ctutil_path_join(&cert_sct_dir, sct_dir, fingerprint, p, s); |
| ap_assert(rv == APR_SUCCESS); |
| |
| if (!ctutil_dir_exists(p, cert_sct_dir)) { |
| rv = apr_dir_make(cert_sct_dir, APR_FPROT_OS_DEFAULT, p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, |
| APLOGNO(02723) "can't create directory %s", |
| cert_sct_dir); |
| ap_assert(rv == APR_SUCCESS); |
| } |
| } |
| |
| rv = ctutil_path_join(&servercerts_pem, cert_sct_dir, |
| SERVERCERTS_BASENAME, p, s); |
| ap_assert(rv == APR_SUCCESS); |
| |
| rv = ctutil_fopen(servercerts_pem, "wb", &concat); |
| ap_assert(rv == APR_SUCCESS); |
| |
| ap_assert(1 == PEM_write_X509(concat, x)); /* leaf */ |
| |
| chain = NULL; |
| |
| /* Not this: SSL_CTX_get0_chain_certs(ctx, &chain); |
| * |
| * See this thread: |
| * http://mail-archives.apache.org/mod_mbox/httpd-dev/ |
| * 201402.mbox/%3CCAKUrXK5-2_Sg8FokxBP8nW7tmSuTZZWL-%3 |
| * DBDhNnwyK-Z4dmQiQ%40mail.gmail.com%3E |
| */ |
| SSL_CTX_get_extra_chain_certs(ctx, &chain); /* UNDOC */ |
| |
| if (chain) { |
| for (i = 0; i < sk_X509_num(chain); i++) { /* UNDOC */ |
| X509 *x = sk_X509_value(chain, i); /* UNDOC */ |
| ap_assert(1 == PEM_write_X509(concat, x)); |
| } |
| } |
| ap_assert(0 == fclose(concat)); |
| |
| ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, |
| APLOGNO(02724) "wrote server cert and chain to %s", |
| servercerts_pem); |
| |
| cert_info = (ct_server_cert_info *)apr_array_push(sconf->server_cert_info); |
| cert_info->sct_dir = cert_sct_dir; |
| cert_info->fingerprint = fingerprint; |
| } |
| else { |
| ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, |
| APLOGNO(02725) "could not find leaf certificate"); |
| } |
| rc = SSL_CTX_set_current_cert(ctx, SSL_CERT_SET_NEXT); |
| } |
| } |
| |
| static ct_conn_config *get_conn_config(conn_rec *c) |
| { |
| ct_conn_config *conncfg = |
| ap_get_module_config(c->conn_config, &ssl_ct_module); |
| |
| if (!conncfg) { |
| conncfg = apr_pcalloc(c->pool, sizeof *conncfg); |
| ap_set_module_config(c->conn_config, &ssl_ct_module, conncfg); |
| } |
| |
| return conncfg; |
| } |
| |
| static void client_is_ct_aware(conn_rec *c) |
| { |
| ct_conn_config *conncfg = get_conn_config(c); |
| conncfg->peer_ct_aware = 1; |
| } |
| |
| static int is_client_ct_aware(conn_rec *c) |
| { |
| ct_conn_config *conncfg = get_conn_config(c); |
| |
| return conncfg->peer_ct_aware; |
| } |
| |
| static void server_cert_has_sct_list(conn_rec *c) |
| { |
| ct_conn_config *conncfg = get_conn_config(c); |
| conncfg->server_cert_has_sct_list = 1; |
| conncfg->peer_ct_aware = 1; |
| } |
| |
| /* Look at SSLClient::VerifyCallback() and WriteSSLClientCTData() |
| * for validation and saving of data for auditing in a form that |
| * the c-t tools can use. |
| */ |
| |
| static cert_chain *cert_chain_init(apr_pool_t *p, STACK_OF(X509) *chain) |
| { |
| cert_chain *cc = apr_pcalloc(p, sizeof(cert_chain)); |
| int i; |
| |
| cc->cert_arr = apr_array_make(p, 4, sizeof(X509 *)); |
| |
| for (i = 0; i < sk_X509_num(chain); i++) { |
| X509 **spot = apr_array_push(cc->cert_arr); |
| *spot = X509_dup(sk_X509_value(chain, i)); /* UNDOC */ |
| if (i == 0) { |
| cc->leaf = *spot; |
| } |
| } |
| |
| return cc; |
| } |
| |
| static void cert_chain_free(cert_chain *cc) |
| { |
| X509 **elts = (X509 **)cc->cert_arr->elts; |
| int i; |
| |
| for (i = 0; i < cc->cert_arr->nelts; i++) { |
| X509_free(elts[i]); |
| } |
| } |
| |
| /* Create hash of leaf certificate and any SCTs so that |
| * we can determine whether or not we've seen this exact |
| * info from the server before. |
| */ |
| static const char *gen_key(conn_rec *c, cert_chain *cc, |
| ct_conn_config *conncfg) |
| { |
| const char *fp; |
| SHA256_CTX sha256ctx; |
| unsigned char digest[SHA256_DIGEST_LENGTH]; |
| |
| fp = get_cert_fingerprint(c->pool, cc->leaf); |
| |
| SHA256_Init(&sha256ctx); /* UNDOC */ |
| SHA256_Update(&sha256ctx, (unsigned char *)fp, strlen(fp)); /* UNDOC */ |
| if (conncfg->cert_sct_list) { |
| SHA256_Update(&sha256ctx, conncfg->cert_sct_list, |
| conncfg->cert_sct_list_size); |
| } |
| if (conncfg->serverhello_sct_list) { |
| SHA256_Update(&sha256ctx, conncfg->serverhello_sct_list, |
| conncfg->serverhello_sct_list_size); |
| } |
| if (conncfg->ocsp_sct_list) { |
| SHA256_Update(&sha256ctx, conncfg->ocsp_sct_list, |
| conncfg->ocsp_sct_list_size); |
| } |
| SHA256_Final(digest, &sha256ctx); /* UNDOC */ |
| return apr_pescape_hex(c->pool, digest, sizeof digest, 0); |
| } |
| |
| static apr_status_t deserialize_SCTs(apr_pool_t *p, |
| ct_conn_config *conncfg, |
| void *sct_list, |
| apr_size_t sct_list_size) |
| { |
| apr_size_t avail, len_of_data; |
| apr_status_t rv; |
| const unsigned char *mem, *start_of_data; |
| |
| mem = sct_list; |
| avail = sct_list_size; |
| |
| /* Make sure the overall length is correct */ |
| |
| rv = ctutil_read_var_bytes((const unsigned char **)&mem, |
| &avail, &start_of_data, &len_of_data); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| if (len_of_data + sizeof(apr_uint16_t) != sct_list_size) { |
| return APR_EINVAL; |
| } |
| |
| /* add each SCT in the list to the all_scts array */ |
| |
| mem = (unsigned char *)sct_list + sizeof(apr_uint16_t); |
| avail = sct_list_size - sizeof(apr_uint16_t); |
| |
| while (rv == APR_SUCCESS && avail > 0) { |
| rv = ctutil_read_var_bytes((const unsigned char **)&mem, &avail, |
| &start_of_data, &len_of_data); |
| if (rv == APR_SUCCESS) { |
| ct_sct_data *sct = (ct_sct_data *)apr_array_push(conncfg->all_scts); |
| |
| sct->data = start_of_data; |
| ap_assert(len_of_data <= USHRT_MAX); |
| sct->len = (apr_uint16_t)len_of_data; |
| } |
| } |
| |
| if (rv == APR_SUCCESS && avail != 0) { |
| return APR_EINVAL; |
| } |
| |
| return APR_SUCCESS; |
| } |
| |
| /* perform quick sanity check of server SCT(s) during handshake; |
| * errors should result in fatal alert |
| */ |
| static apr_status_t validate_server_data(apr_pool_t *p, conn_rec *c, |
| cert_chain *cc, ct_conn_config *conncfg, |
| ct_server_config *sconf) |
| { |
| apr_status_t rv = APR_SUCCESS; |
| |
| if (conncfg->serverhello_sct_list) { |
| ap_log_cdata(APLOG_MARK, APLOG_TRACE6, c, "SCT(s) from ServerHello", |
| conncfg->serverhello_sct_list, |
| conncfg->serverhello_sct_list_size, |
| AP_LOG_DATA_SHOW_OFFSET); |
| } |
| |
| if (conncfg->cert_sct_list) { |
| ap_log_cdata(APLOG_MARK, APLOG_TRACE6, c, "SCT(s) from certificate", |
| conncfg->cert_sct_list, |
| conncfg->cert_sct_list_size, |
| AP_LOG_DATA_SHOW_OFFSET); |
| } |
| |
| if (conncfg->ocsp_sct_list) { |
| ap_log_cdata(APLOG_MARK, APLOG_TRACE6, c, "SCT(s) from stapled OCSP response", |
| conncfg->ocsp_sct_list, |
| conncfg->ocsp_sct_list_size, |
| AP_LOG_DATA_SHOW_OFFSET); |
| } |
| |
| if (!conncfg->all_scts) { |
| conncfg->all_scts = apr_array_make(p, 4, sizeof(ct_sct_data)); |
| } |
| |
| /* deserialize all the SCTs */ |
| if (conncfg->cert_sct_list) { |
| rv = deserialize_SCTs(p, conncfg, conncfg->cert_sct_list, |
| conncfg->cert_sct_list_size); |
| if (rv != APR_SUCCESS) { |
| ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, c, |
| APLOGNO(02726) "couldn't deserialize SCT list from " |
| "certificate"); |
| } |
| } |
| if (rv == APR_SUCCESS && conncfg->serverhello_sct_list) { |
| rv = deserialize_SCTs(p, conncfg, conncfg->serverhello_sct_list, |
| conncfg->serverhello_sct_list_size); |
| if (rv != APR_SUCCESS) { |
| ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, c, |
| APLOGNO(02727) "couldn't deserialize SCT list from " |
| "ServerHello"); |
| } |
| } |
| if (rv == APR_SUCCESS && conncfg->ocsp_sct_list) { |
| rv = deserialize_SCTs(p, conncfg, conncfg->ocsp_sct_list, |
| conncfg->ocsp_sct_list_size); |
| if (rv != APR_SUCCESS) { |
| ap_log_cerror(APLOG_MARK, APLOG_ERR, rv, c, |
| APLOGNO(02728) "couldn't deserialize SCT list from " |
| "stapled OCSP response"); |
| } |
| } |
| |
| if (rv == APR_SUCCESS) { |
| if (conncfg->all_scts->nelts < 1) { |
| /* How did we get here without at least one SCT? */ |
| ap_log_cerror(APLOG_MARK, APLOG_CRIT, 0, c, |
| APLOGNO(02729) "SNAFU: No deserialized SCTs found in " |
| "validate_server_data()"); |
| rv = APR_EINVAL; |
| } |
| else { |
| apr_status_t tmprv; |
| int i, verification_failures, verification_successes, unknown_log_ids; |
| ct_sct_data *sct_elts; |
| ct_sct_data sct; |
| sct_fields_t fields; |
| |
| ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03028) |
| "%d SCTs received total", conncfg->all_scts->nelts); |
| |
| verification_failures = verification_successes = unknown_log_ids = 0; |
| sct_elts = (ct_sct_data *)conncfg->all_scts->elts; |
| for (i = 0; i < conncfg->all_scts->nelts; i++) { |
| sct = sct_elts[i]; |
| tmprv = sct_parse("backend server", c->base_server, |
| sct.data, sct.len, cc, |
| &fields); |
| if (tmprv != APR_SUCCESS) { |
| rv = tmprv; |
| } |
| else { |
| tmprv = sct_verify_timestamp(c, &fields); |
| if (tmprv != APR_SUCCESS) { |
| verification_failures++; |
| } |
| |
| if (active_log_config) { |
| /* will only block if we have a DB-based log |
| * configuration which is currently being refreshed |
| */ |
| ap_assert(apr_thread_rwlock_rdlock(log_config_rwlock) |
| == APR_SUCCESS); |
| tmprv = sct_verify_signature(c, &fields, |
| active_log_config); |
| ap_assert(apr_thread_rwlock_unlock(log_config_rwlock) |
| == APR_SUCCESS); |
| if (tmprv == APR_NOTFOUND) { |
| ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c, |
| APLOGNO(02730) "Server sent SCT from " |
| "unrecognized log"); |
| unknown_log_ids++; |
| } |
| else if (tmprv != APR_SUCCESS) { |
| ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, |
| APLOGNO(02731) "Server sent SCT with " |
| "invalid signature"); |
| tmprv = APR_EINVAL; |
| verification_failures++; |
| } |
| else { |
| verification_successes++; |
| } |
| } |
| else { |
| unknown_log_ids++; |
| ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c, |
| APLOGNO(02732) "Signature of SCT from " |
| "server could not be verified (no " |
| "configured log public keys)"); |
| } |
| } |
| sct_release(&fields); |
| } |
| if (verification_failures && !verification_successes) { |
| /* If no SCTs are valid, don't communicate. */ |
| rv = APR_EINVAL; |
| } |
| ap_log_cerror(APLOG_MARK, |
| rv != APR_SUCCESS ? APLOG_ERR : APLOG_INFO, 0, c, |
| APLOGNO(02733) "Signature/timestamp validation for %d " |
| "SCTs: %d successes, " |
| "%d failures, %d from unknown logs", |
| conncfg->all_scts->nelts, verification_successes, |
| verification_failures, unknown_log_ids); |
| } |
| } |
| |
| return rv; |
| } |
| |
| /* Enqueue data from server for off-line audit (cert, SCT(s)) |
| * We already filtered out duplicate data being saved from this |
| * process. (With reverse proxy it will be the same data over |
| * and over.) |
| */ |
| #define SERVER_START 0x0001 |
| #define KEY_START 0x0002 |
| #define CERT_START 0x0003 |
| #define SCT_START 0x0004 |
| |
| static void save_server_data(conn_rec *c, cert_chain *cc, |
| ct_conn_config *conncfg, |
| const char *key) |
| { |
| if (audit_file_mutex && audit_file) { /* child init successful, no |
| * subsequent error |
| */ |
| apr_size_t bytes_written; |
| apr_status_t rv; |
| int i; |
| ct_sct_data *sct_elts; |
| X509 **x509elts; |
| server_rec *s = c->base_server; |
| |
| /* Any error in this function is a file I/O error; |
| * if such an error occurs, the audit file will be closed |
| * and removed, and this child won't be able to queue |
| * anything for audit. (It is likely that other child |
| * processes will have the same problem.) |
| */ |
| |
| ctutil_thread_mutex_lock(audit_file_mutex); |
| |
| if (audit_file) { /* no error just occurred... */ |
| audit_file_nonempty = 1; |
| |
| rv = ctutil_file_write_uint16(s, audit_file, |
| SERVER_START); |
| |
| if (rv == APR_SUCCESS) { |
| rv = ctutil_file_write_uint16(s, audit_file, KEY_START); |
| } |
| |
| if (rv == APR_SUCCESS) { |
| ap_assert(strlen(key) <= USHRT_MAX); |
| rv = ctutil_file_write_uint16(s, audit_file, |
| (apr_uint16_t)strlen(key)); |
| } |
| |
| if (rv == APR_SUCCESS) { |
| rv = apr_file_write_full(audit_file, key, strlen(key), |
| &bytes_written); |
| } |
| |
| /* Write each certificate, starting with leaf */ |
| x509elts = (X509 **)cc->cert_arr->elts; |
| for (i = 0; rv == APR_SUCCESS && i < cc->cert_arr->nelts; i++) { |
| unsigned char *der_buf = NULL; |
| int der_length; |
| |
| rv = ctutil_file_write_uint16(s, audit_file, CERT_START); |
| |
| /* now write the cert!!! */ |
| |
| if (rv == APR_SUCCESS) { |
| der_length = i2d_X509(x509elts[i], &der_buf); |
| ap_assert(der_length > 0); |
| |
| rv = ctutil_file_write_uint24(s, audit_file, der_length); |
| } |
| |
| if (rv == APR_SUCCESS) { |
| rv = apr_file_write_full(audit_file, der_buf, der_length, |
| &bytes_written); |
| } |
| |
| OPENSSL_free(der_buf); |
| } |
| |
| /* Write each SCT */ |
| sct_elts = (ct_sct_data *)conncfg->all_scts->elts; |
| for (i = 0; rv == APR_SUCCESS && i < conncfg->all_scts->nelts; i++) { |
| ct_sct_data sct; |
| |
| rv = ctutil_file_write_uint16(s, audit_file, SCT_START); |
| |
| sct = sct_elts[i]; |
| |
| if (rv == APR_SUCCESS) { |
| rv = ctutil_file_write_uint16(s, audit_file, sct.len); |
| } |
| |
| if (rv == APR_SUCCESS) { |
| rv = apr_file_write_full(audit_file, sct.data, sct.len, |
| &bytes_written); |
| } |
| } |
| |
| if (rv != APR_SUCCESS) { |
| /* an I/O error occurred; file is not usable */ |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, ap_server_conf, |
| APLOGNO(02734) "Failed to write to %s, disabling " |
| "audit for this child", audit_fn_active); |
| apr_file_close(audit_file); |
| audit_file = NULL; |
| apr_file_remove(audit_fn_active, |
| /* not used in current implementations */ |
| c->pool); |
| } |
| } |
| |
| ctutil_thread_mutex_unlock(audit_file_mutex); |
| } |
| } |
| |
| /* signed_certificate_timestamp */ |
| static const unsigned short CT_EXTENSION_TYPE = 18; |
| |
| /* See function of this name in openssl/apps/s_client.c */ |
| static int ocsp_resp_cb(SSL *ssl, void *arg) |
| { |
| conn_rec *c = (conn_rec *)SSL_get_app_data(ssl); |
| ct_conn_config *conncfg = get_conn_config(c); |
| const unsigned char *p; |
| int i, len; |
| OCSP_RESPONSE *rsp; |
| OCSP_BASICRESP *br; |
| OCSP_SINGLERESP *single; |
| |
| len = SSL_get_tlsext_status_ocsp_resp(ssl, &p); /* UNDOC */ |
| if (!p) { |
| /* normal case */ |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, |
| "OCSP response callback called but no stapled response from server"); |
| return 1; |
| } |
| |
| rsp = d2i_OCSP_RESPONSE(NULL, &p, len); /* UNDOC */ |
| if (!rsp) { |
| ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, |
| APLOGNO(02792) "Error parsing OCSP response"); |
| return 0; |
| } |
| |
| br = OCSP_response_get1_basic(rsp); /* UNDOC */ |
| if (!br) { |
| ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03029) |
| "no OCSP basic response"); |
| return 0; |
| } |
| |
| for (i = 0; i < OCSP_resp_count(br); i++) { |
| const unsigned char *p; |
| X509_EXTENSION *ext; |
| int idx; |
| ASN1_OCTET_STRING *oct1, *oct2; |
| |
| single = OCSP_resp_get0(br, i); |
| if (!single) { |
| continue; |
| } |
| |
| idx = OCSP_SINGLERESP_get_ext_by_NID(single, |
| NID_ct_cert_scts, -1); /* UNDOC */ |
| |
| if (idx == -1) { |
| continue; |
| } |
| |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, |
| "index of NID_ct_cert_scts: %d", idx); |
| |
| ext = OCSP_SINGLERESP_get_ext(single, idx); |
| oct1 = X509_EXTENSION_get_data(ext); /* UNDOC */ |
| |
| p = oct1->data; |
| if ((oct2 = d2i_ASN1_OCTET_STRING(NULL, &p, oct1->length)) != NULL) { |
| conncfg->ocsp_has_sct_list = 1; |
| conncfg->peer_ct_aware = 1; |
| conncfg->ocsp_sct_list_size = oct2->length; |
| conncfg->ocsp_sct_list = apr_pmemdup(c->pool, oct2->data, |
| conncfg->ocsp_sct_list_size); |
| ASN1_OCTET_STRING_free(oct2); |
| } |
| } |
| |
| OCSP_RESPONSE_free(rsp); /* UNDOC */ |
| |
| return 1; |
| } |
| |
| /* Callbacks and structures for handling custom TLS Extensions: |
| * client_extension_add_callback - sends data for ClientHello TLS Extension |
| * client_extension_parse_callback - receives data from ServerHello TLS Extension |
| */ |
| static int client_extension_add_callback(SSL *ssl, unsigned ext_type, |
| const unsigned char **out, |
| size_t *outlen, int *al, |
| void *arg) |
| { |
| conn_rec *c = (conn_rec *)SSL_get_app_data(ssl); |
| |
| /* nothing to send in ClientHello */ |
| |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, |
| "client_extension_add_callback called, " |
| "ext %hu will be in ClientHello", |
| ext_type); |
| |
| return 1; |
| } |
| |
| /* Get SCT(s) from ServerHello */ |
| static int client_extension_parse_callback(SSL *ssl, unsigned ext_type, |
| const unsigned char *in, size_t inlen, |
| int *al, void *arg) |
| { |
| conn_rec *c = (conn_rec *)SSL_get_app_data(ssl); |
| ct_conn_config *conncfg = get_conn_config(c); |
| |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, |
| "client_extension_parse_callback called, " |
| "ext %u was in ServerHello (len %" APR_SIZE_T_FMT ")", |
| ext_type, inlen); |
| |
| /* Note: Peer certificate is not available in this callback via |
| * SSL_get_peer_certificate(ssl) |
| */ |
| |
| conncfg->serverhello_has_sct_list = 1; |
| conncfg->peer_ct_aware = 1; |
| conncfg->serverhello_sct_list = apr_pmemdup(c->pool, in, inlen); |
| conncfg->serverhello_sct_list_size = inlen; |
| return 1; |
| } |
| |
| /* See SSLClient::VerifyCallback() in c-t/src/client/ssl_client.cc |
| * (That's a beast and hard to duplicate in depth when you consider |
| * all the support classes it relies on; mod_ssl_ct needs to be a |
| * C++ module so that the bugs are fixed in one place.) |
| * |
| * . This code should care about stapled SCTs but doesn't. |
| * . This code, unlike SSLClient::VerifyCallback(), doesn't look |
| * at the OpenSSL "input" chain. |
| */ |
| static int ssl_ct_ssl_proxy_verify(server_rec *s, conn_rec *c, |
| STACK_OF(X509) *chain) |
| { |
| apr_pool_t *p = c->pool; |
| ct_conn_config *conncfg = get_conn_config(c); |
| ct_server_config *sconf = ap_get_module_config(s->module_config, |
| &ssl_ct_module); |
| int chain_size = sk_X509_num(chain); |
| int extension_index; |
| cert_chain *certs; |
| |
| if (sconf->proxy_awareness == PROXY_OBLIVIOUS) { |
| return OK; |
| } |
| |
| ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03030) |
| "ssl_ct_ssl_proxy_verify() - get server certificate info"); |
| |
| if (chain_size < 1) { |
| ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, |
| APLOGNO(02735) "odd chain size %d -- cannot proceed", |
| chain_size); |
| return APR_EINVAL; |
| } |
| |
| /* Note: SSLClient::Verify looks in both the input chain and the |
| * verified chain. |
| */ |
| |
| certs = cert_chain_init(p, chain); |
| conncfg->certs = certs; |
| |
| extension_index = |
| X509_get_ext_by_NID(certs->leaf, |
| NID_ct_precert_scts, |
| -1); |
| /* use X509_get_ext(certs->leaf, extension_index) to obtain X509_EXTENSION * */ |
| |
| if (extension_index >= 0) { |
| void *ext_struct; |
| |
| server_cert_has_sct_list(c); |
| /* as in Cert::ExtensionStructure() */ |
| ext_struct = X509_get_ext_d2i(certs->leaf, |
| NID_ct_precert_scts, |
| NULL, /* ignore criticality of extension */ |
| NULL); /* UNDOC */ |
| |
| if (ext_struct == NULL) { |
| ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, |
| APLOGNO(02736) "Could not retrieve SCT list from " |
| "certificate (unexpected)"); |
| } |
| else { |
| /* as in Cert::OctetStringExtensionData */ |
| ASN1_OCTET_STRING *octet = (ASN1_OCTET_STRING *)ext_struct; |
| conncfg->cert_sct_list = apr_pmemdup(p, |
| octet->data, |
| octet->length); |
| conncfg->cert_sct_list_size = octet->length; |
| ASN1_OCTET_STRING_free(octet); /* UNDOC */ |
| } |
| } |
| |
| return OK; |
| } |
| |
| static int ssl_ct_proxy_post_handshake(conn_rec *c, SSL *ssl) |
| { |
| apr_pool_t *p = c->pool; |
| apr_status_t rv = APR_SUCCESS; |
| const char *key; |
| ct_cached_server_data *cached = NULL; |
| ct_conn_config *conncfg = get_conn_config(c); |
| server_rec *s = c->base_server; |
| ct_server_config *sconf = ap_get_module_config(s->module_config, |
| &ssl_ct_module); |
| int validation_error = 0, missing_sct_error = 0; |
| STACK_OF(X509) *chain = SSL_get_peer_cert_chain(ssl); |
| |
| if (sconf->proxy_awareness == PROXY_OBLIVIOUS) { |
| return OK; |
| } |
| |
| ssl_ct_ssl_proxy_verify(s, c, chain); |
| |
| ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03247) |
| "finally at the point where we can see where SCTs came from" |
| " %pp/%pp/%pp (c %pp)", |
| conncfg->cert_sct_list, conncfg->serverhello_sct_list, |
| conncfg->ocsp_sct_list, c); |
| |
| /* At this point we have the SCTs from the cert (if any) and the |
| * SCTs from the TLS extension (if any) in ct_conn_config. |
| */ |
| |
| if (conncfg->cert_sct_list || conncfg->serverhello_sct_list |
| || conncfg->ocsp_sct_list) { |
| |
| /* The key is critical to avoiding validating and queueing of |
| * the same stuff over and over. |
| * |
| * Is there any cheaper check than server cert and SCTs all exactly |
| * the same as before? |
| */ |
| |
| key = gen_key(c, conncfg->certs, conncfg); |
| |
| ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03031) |
| "key for server data: %s", key); |
| |
| ctutil_thread_mutex_lock(cached_server_data_mutex); |
| |
| cached = apr_hash_get(cached_server_data, key, APR_HASH_KEY_STRING); |
| |
| ctutil_thread_mutex_unlock(cached_server_data_mutex); |
| |
| if (!cached) { |
| ct_cached_server_data *new_server_data = |
| (ct_cached_server_data *)calloc(1, sizeof(ct_cached_server_data)); |
| |
| new_server_data->validation_result = |
| rv = validate_server_data(p, c, conncfg->certs, conncfg, sconf); |
| |
| if (rv != APR_SUCCESS) { |
| validation_error = 1; |
| } |
| |
| ctutil_thread_mutex_lock(cached_server_data_mutex); |
| |
| if ((cached = apr_hash_get(cached_server_data, key, APR_HASH_KEY_STRING))) { |
| /* some other thread snuck in |
| * we assume that the other thread got the same validation |
| * result that we did |
| */ |
| free(new_server_data); |
| new_server_data = NULL; |
| } |
| else { |
| /* no other thread snuck in */ |
| apr_hash_set(cached_server_data, key, APR_HASH_KEY_STRING, |
| new_server_data); |
| new_server_data = NULL; |
| } |
| |
| ctutil_thread_mutex_unlock(cached_server_data_mutex); |
| |
| if (rv == APR_SUCCESS && !cached) { |
| save_server_data(c, conncfg->certs, conncfg, key); |
| } |
| } |
| else { |
| /* cached */ |
| rv = cached->validation_result; |
| if (rv != APR_SUCCESS) { |
| validation_error = 1; |
| ap_log_cerror(APLOG_MARK, APLOG_INFO, rv, c, |
| APLOGNO(02737) "bad cached validation result"); |
| } |
| } |
| } |
| else { |
| /* No SCTs at all; consult configuration to know what to do. */ |
| missing_sct_error = 1; |
| } |
| |
| if (conncfg->certs) { |
| cert_chain_free(conncfg->certs); |
| conncfg->certs = NULL; |
| } |
| |
| ap_log_cerror(APLOG_MARK, |
| rv == APR_SUCCESS ? APLOG_DEBUG : APLOG_ERR, rv, c, |
| APLOGNO(02738) "SCT list received in: %s%s%s(%s) (c %pp)", |
| conncfg->serverhello_has_sct_list ? "ServerHello " : "", |
| conncfg->server_cert_has_sct_list ? "certificate-extension " : "", |
| conncfg->ocsp_has_sct_list ? "OCSP " : "", |
| cached ? "already saved" : "seen for the first time", |
| c); |
| |
| if (sconf->proxy_awareness == PROXY_REQUIRE) { |
| if (missing_sct_error || validation_error) { |
| ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c, |
| APLOGNO(02739) "Forbidding access to backend server; " |
| "no valid SCTs"); |
| return HTTP_FORBIDDEN; |
| } |
| } |
| |
| return OK; |
| } |
| |
| static int server_extension_parse_callback(SSL *ssl, unsigned ext_type, |
| const unsigned char *in, |
| size_t inlen, int *al, |
| void *arg) |
| { |
| conn_rec *c = (conn_rec *)SSL_get_app_data(ssl); |
| |
| /* this callback tells us that client is CT-aware; |
| * there's nothing of interest in the extension data |
| */ |
| client_is_ct_aware(c); |
| |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, |
| "server_extension_parse_callback called, " |
| "ext %u was in ClientHello (len %" APR_SIZE_T_FMT ")", |
| ext_type, inlen); |
| |
| return 1; |
| } |
| |
| static int server_extension_add_callback(SSL *ssl, unsigned ext_type, |
| const unsigned char **out, |
| size_t *outlen, int *al, |
| void *arg) |
| { |
| conn_rec *c = (conn_rec *)SSL_get_app_data(ssl); |
| ct_server_config *sconf = ap_get_module_config(c->base_server->module_config, |
| &ssl_ct_module); |
| X509 *server_cert; |
| const char *fingerprint; |
| const unsigned char *scts; |
| apr_size_t scts_len; |
| apr_status_t rv; |
| |
| if (!is_client_ct_aware(c)) { |
| /* Hmmm... Is this actually called if the client doesn't include |
| * the extension in the ClientHello? I don't think so. |
| */ |
| ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03032) |
| "server_extension_callback_2: client isn't CT-aware"); |
| /* Skip this extension for ServerHello */ |
| return 0; |
| } |
| |
| /* need to reply with SCT */ |
| |
| server_cert = SSL_get_certificate(ssl); /* no need to free! */ |
| fingerprint = get_cert_fingerprint(c->pool, server_cert); |
| |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, |
| "server_extension_add_callback called, " |
| "ext %hu will be in ServerHello", |
| ext_type); |
| |
| rv = read_scts(c->pool, fingerprint, |
| sconf->sct_storage, |
| c->base_server, (char **)&scts, &scts_len); |
| if (rv == APR_SUCCESS) { |
| *out = scts; |
| ap_assert(scts_len <= USHRT_MAX); |
| *outlen = (unsigned short)scts_len; |
| } |
| else { |
| /* Skip this extension for ServerHello */ |
| return 0; |
| } |
| |
| return 1; |
| } |
| |
| static void tlsext_cb(SSL *ssl, int client_server, int type, |
| unsigned char *data, int len, |
| void *arg) |
| { |
| conn_rec *c = arg; |
| |
| if (type == CT_EXTENSION_TYPE) { |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, |
| "tlsext_cb called, got CT TLS extension"); |
| |
| client_is_ct_aware(c); |
| } |
| } |
| |
| static int ssl_ct_pre_handshake(conn_rec *c, SSL *ssl, int is_proxy) |
| { |
| ct_conn_config *conncfg = get_conn_config(c); |
| |
| if (is_proxy) { |
| conncfg->proxy_handshake = 1; |
| } |
| else { |
| conncfg->client_handshake = 1; |
| } |
| |
| ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03033) |
| "client connected (pre-handshake)"); |
| |
| SSL_set_tlsext_status_type(ssl, TLSEXT_STATUSTYPE_ocsp); /* UNDOC */ |
| |
| /* This callback is needed only to determine that the peer is CT-aware |
| * when resuming a session. For an initial handshake, the callbacks |
| * registered via SSL_CTX_set_custom_srv_ext() are sufficient. |
| */ |
| SSL_set_tlsext_debug_callback(ssl, tlsext_cb); /* UNDOC */ |
| SSL_set_tlsext_debug_arg(ssl, c); /* UNDOC */ |
| |
| return OK; |
| } |
| |
| static int ssl_ct_init_server(server_rec *s, apr_pool_t *p, int is_proxy, |
| SSL_CTX *ssl_ctx) |
| { |
| ct_callback_info *cbi = apr_pcalloc(p, sizeof *cbi); |
| ct_server_config *sconf = ap_get_module_config(s->module_config, |
| &ssl_ct_module); |
| |
| if (s != ap_server_conf) { |
| ct_server_config *main_conf = |
| ap_get_module_config(ap_server_conf->module_config, |
| &ssl_ct_module); |
| |
| if (sconf == main_conf) { |
| /* There weren't any directives for this module in the vhost, |
| * so core httpd gave us the global scope's module config. |
| * We need to be able to represent some mod_ssl-related |
| * config (certs) that are generally configured in the vhost, |
| * so we have to create a vhost-specific module config. |
| */ |
| sconf = copy_ct_server_config(p, main_conf); |
| ap_set_module_config(s->module_config, &ssl_ct_module, sconf); |
| } |
| } |
| |
| cbi->s = s; |
| |
| if (is_proxy && sconf->proxy_awareness != PROXY_OBLIVIOUS) { |
| if (!SSL_CTX_add_client_custom_ext(ssl_ctx, CT_EXTENSION_TYPE, |
| client_extension_add_callback, |
| NULL, NULL, |
| client_extension_parse_callback, cbi)) { |
| ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, |
| APLOGNO(02740) "Unable to initialize Certificate " |
| "Transparency client extension callbacks " |
| "(callback for %d already registered?)", |
| CT_EXTENSION_TYPE); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| |
| /* Uhh, hopefully this doesn't collide with anybody else. mod_ssl |
| * currently only sets this on the server SSL_CTX, when OCSP is |
| * enabled. |
| */ |
| SSL_CTX_set_tlsext_status_cb(ssl_ctx, ocsp_resp_cb); /* UNDOC */ |
| SSL_CTX_set_tlsext_status_arg(ssl_ctx, cbi); /* UNDOC */ |
| } |
| else if (!is_proxy) { |
| look_for_server_certs(s, ssl_ctx, sconf->sct_storage); |
| |
| if (!SSL_CTX_add_server_custom_ext(ssl_ctx, CT_EXTENSION_TYPE, |
| server_extension_add_callback, |
| NULL, NULL, |
| server_extension_parse_callback, cbi)) { |
| ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, |
| APLOGNO(02741) "Unable to initialize Certificate " |
| "Transparency server extension callback " |
| "(callbacks for %d already registered?)", |
| CT_EXTENSION_TYPE); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| } |
| |
| return OK; |
| } |
| |
| static int ssl_ct_post_read_request(request_rec *r) |
| { |
| ct_conn_config *conncfg = |
| ap_get_module_config(r->connection->conn_config, &ssl_ct_module); |
| |
| if (conncfg) { |
| if (conncfg->client_handshake) { |
| apr_table_set(r->subprocess_env, CLIENT_STATUS_VAR, |
| conncfg->peer_ct_aware ? |
| STATUS_VAR_AWARE_VAL : STATUS_VAR_UNAWARE_VAL); |
| } |
| /* else no SSL on this client connection */ |
| } |
| |
| return DECLINED; |
| } |
| |
| static int ssl_ct_pre_config(apr_pool_t *pconf, apr_pool_t *plog, |
| apr_pool_t *ptemp) |
| { |
| apr_status_t rv = ap_mutex_register(pconf, SSL_CT_MUTEX_TYPE, NULL, |
| APR_LOCK_DEFAULT, 0); |
| if (rv != APR_SUCCESS) { |
| return rv; |
| } |
| |
| apr_dbd_init(pconf); |
| |
| ctutil_run_internal_tests(ptemp); |
| |
| return OK; |
| } |
| |
| static apr_status_t inactivate_audit_file(void *data) |
| { |
| apr_status_t rv; |
| server_rec *s = data; |
| |
| if (!audit_file) { /* something bad happened after child init */ |
| return APR_SUCCESS; |
| } |
| |
| /* the normal cleanup was disabled in the call to apr_file_open */ |
| rv = apr_file_close(audit_file); |
| audit_file = NULL; |
| if (rv == APR_SUCCESS) { |
| if (audit_file_nonempty) { |
| rv = apr_file_rename(audit_fn_active, audit_fn_perm, |
| /* not used in current implementations */ |
| s->process->pool); |
| } |
| else { |
| /* No data written; just remove the file */ |
| apr_file_remove(audit_fn_active, |
| /* not used in current implementations */ |
| s->process->pool); |
| } |
| } |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, |
| APLOGNO(02742) "error flushing/closing %s or renaming it " |
| "to %s", |
| audit_fn_active, audit_fn_perm); |
| } |
| |
| return APR_SUCCESS; /* what, you think anybody cares? */ |
| } |
| |
| static void ssl_ct_child_init(apr_pool_t *p, server_rec *s) |
| { |
| apr_status_t rv; |
| const char *audit_basename; |
| ct_server_config *sconf = ap_get_module_config(s->module_config, |
| &ssl_ct_module); |
| |
| cached_server_data = apr_hash_make(p); |
| |
| rv = apr_global_mutex_child_init(&ssl_ct_sct_update, |
| apr_global_mutex_lockfile(ssl_ct_sct_update), p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, |
| APLOGNO(02743) "could not initialize " SSL_CT_MUTEX_TYPE |
| " mutex in child"); |
| /* might crash otherwise due to lack of checking for initialized data |
| * in all the right places, but this is going to skip pchild cleanup |
| */ |
| exit(APEXIT_CHILDSICK); |
| } |
| |
| rv = apr_thread_rwlock_create(&log_config_rwlock, p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, |
| APLOGNO(02744) "could not create rwlock in child"); |
| exit(APEXIT_CHILDSICK); |
| } |
| |
| rv = ap_thread_create(&service_thread, NULL, run_service_thread, s, p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, |
| APLOGNO(02745) "could not create " SERVICE_THREAD_NAME |
| " in child"); |
| /* might crash otherwise due to lack of checking for initialized data |
| * in all the right places, but this is going to skip pchild cleanup |
| */ |
| exit(APEXIT_CHILDSICK); |
| } |
| |
| apr_pool_pre_cleanup_register(p, service_thread, wait_for_thread); |
| |
| if (sconf->proxy_awareness != PROXY_OBLIVIOUS) { |
| rv = apr_thread_mutex_create(&cached_server_data_mutex, |
| APR_THREAD_MUTEX_DEFAULT, |
| p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, |
| APLOGNO(02746) "could not allocate a thread mutex"); |
| /* might crash otherwise due to lack of checking for initialized data |
| * in all the right places, but this is going to skip pchild cleanup |
| */ |
| exit(APEXIT_CHILDSICK); |
| } |
| } |
| |
| if (sconf->proxy_awareness != PROXY_OBLIVIOUS && sconf->audit_storage) { |
| rv = apr_thread_mutex_create(&audit_file_mutex, |
| APR_THREAD_MUTEX_DEFAULT, p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, |
| APLOGNO(02747) "could not allocate a thread mutex"); |
| /* might crash otherwise due to lack of checking for initialized data |
| * in all the right places, but this is going to skip pchild cleanup |
| */ |
| exit(APEXIT_CHILDSICK); |
| } |
| |
| audit_basename = apr_psprintf(p, "audit_%" APR_PID_T_FMT, |
| getpid()); |
| rv = ctutil_path_join((char **)&audit_fn_perm, sconf->audit_storage, |
| audit_basename, p, s); |
| if (rv != APR_SUCCESS) { |
| /* might crash otherwise due to lack of checking for initialized data |
| * in all the right places, but this is going to skip pchild cleanup |
| */ |
| exit(APEXIT_CHILDSICK); |
| } |
| |
| audit_fn_active = apr_pstrcat(p, audit_fn_perm, ".tmp", NULL); |
| audit_fn_perm = apr_pstrcat(p, audit_fn_perm, ".out", NULL); |
| |
| if (ctutil_file_exists(p, audit_fn_active)) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s, |
| APLOGNO(02748) "Pid-specific file %s was reused before " |
| "audit grabbed it! (removing)", |
| audit_fn_active); |
| apr_file_remove(audit_fn_active, p); |
| } |
| |
| if (ctutil_file_exists(p, audit_fn_perm)) { |
| ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s, |
| APLOGNO(02749) "Pid-specific file %s was reused before " |
| "audit grabbed it! (removing)", |
| audit_fn_perm); |
| apr_file_remove(audit_fn_perm, p); |
| } |
| |
| rv = apr_file_open(&audit_file, audit_fn_active, |
| APR_FOPEN_WRITE|APR_FOPEN_CREATE|APR_FOPEN_TRUNCATE |
| |APR_FOPEN_BINARY|APR_FOPEN_BUFFERED|APR_FOPEN_NOCLEANUP, |
| APR_FPROT_OS_DEFAULT, p); |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, |
| APLOGNO(02750) "can't create %s", audit_fn_active); |
| audit_file = NULL; |
| } |
| |
| if (audit_file) { |
| apr_pool_cleanup_register(p, s, inactivate_audit_file, apr_pool_cleanup_null); |
| } |
| } /* !PROXY_OBLIVIOUS */ |
| } |
| |
| static void *create_ct_server_config(apr_pool_t *p, server_rec *s) |
| { |
| ct_server_config *conf = |
| (ct_server_config *)apr_pcalloc(p, sizeof(ct_server_config)); |
| |
| conf->max_sct_age = apr_time_from_sec(3600 * 24); |
| conf->proxy_awareness = PROXY_AWARENESS_UNSET; |
| conf->max_sh_sct = 100; |
| conf->static_cert_sct_dirs = apr_hash_make(p); |
| |
| return conf; |
| } |
| |
| static void *merge_ct_server_config(apr_pool_t *p, void *basev, void *virtv) |
| { |
| ct_server_config *base = (ct_server_config *)basev; |
| ct_server_config *virt = (ct_server_config *)virtv; |
| ct_server_config *conf; |
| |
| conf = (ct_server_config *)apr_pmemdup(p, virt, sizeof(ct_server_config)); |
| |
| /* copy non-per-vhost fields from base (other than a few that aren't |
| * referenced from per-vhost config) |
| */ |
| conf->sct_storage = base->sct_storage; |
| conf->audit_storage = base->audit_storage; |
| conf->ct_exe = base->ct_exe; |
| conf->max_sct_age = base->max_sct_age; |
| conf->log_config_fname = base->log_config_fname; |
| conf->db_log_config = base->db_log_config; |
| conf->static_log_config = base->static_log_config; |
| conf->max_sh_sct = base->max_sh_sct; |
| conf->static_cert_sct_dirs = base->static_cert_sct_dirs; |
| |
| conf->proxy_awareness = (virt->proxy_awareness != PROXY_AWARENESS_UNSET) |
| ? virt->proxy_awareness |
| : base->proxy_awareness; |
| |
| return conf; |
| } |
| |
| static ct_server_config *copy_ct_server_config(apr_pool_t *p, |
| ct_server_config *base) |
| { |
| /* make a copy of the existing server config and initialize anything |
| * that is per-vhost |
| */ |
| ct_server_config *sconf = |
| (ct_server_config *)apr_pmemdup(p, base, sizeof(ct_server_config)); |
| sconf->server_cert_info = NULL; |
| return sconf; |
| } |
| |
| #if AP_MODULE_MAGIC_AT_LEAST(20140207,2) |
| /* Only trunk has the proxy_detach_backend hook; without it, |
| * no way to set the envvars which represent backend CT status |
| */ |
| static int ssl_ct_detach_backend(request_rec *r, |
| proxy_conn_rec *backend) |
| { |
| conn_rec *origin = backend->connection; |
| |
| if (origin) { |
| ct_conn_config *conncfg = get_conn_config(origin); |
| char *list, *last; |
| |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(03034) |
| "ssl_ct_detach_backend, %d%d%d", |
| conncfg->server_cert_has_sct_list, |
| conncfg->serverhello_has_sct_list, |
| conncfg->ocsp_has_sct_list); |
| |
| if (conncfg->proxy_handshake) { |
| apr_table_set(r->subprocess_env, PROXY_STATUS_VAR, |
| conncfg->peer_ct_aware ? |
| STATUS_VAR_AWARE_VAL : STATUS_VAR_UNAWARE_VAL); |
| |
| list = apr_pstrcat(r->pool, |
| conncfg->server_cert_has_sct_list ? "certext," : "", |
| conncfg->serverhello_has_sct_list ? "tlsext," : "", |
| conncfg->ocsp_has_sct_list ? "ocsp" : "", |
| NULL); |
| if (*list) { |
| last = list + strlen(list) - 1; |
| if (*last == ',') { |
| *last = '\0'; |
| } |
| } |
| |
| apr_table_set(r->subprocess_env, PROXY_SCT_SOURCES_VAR, list); |
| } |
| } |
| else { |
| /* why here? some odd error path? */ |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(03035) |
| "No backend connection available in ssl_ct_detach_backend()"); |
| } |
| |
| return OK; |
| } |
| #endif |
| |
| static void ct_register_hooks(apr_pool_t *p) |
| { |
| static const char * const run_after_mod_ssl[] = {"mod_ssl.c", NULL}; |
| |
| ap_hook_pre_config(ssl_ct_pre_config, NULL, NULL, APR_HOOK_MIDDLE); |
| ap_hook_check_config(ssl_ct_check_config, NULL, NULL, APR_HOOK_MIDDLE); |
| ap_hook_post_config(ssl_ct_post_config, run_after_mod_ssl, NULL, |
| APR_HOOK_MIDDLE); |
| ap_hook_post_read_request(ssl_ct_post_read_request, NULL, NULL, APR_HOOK_MIDDLE); |
| ap_hook_child_init(ssl_ct_child_init, NULL, NULL, APR_HOOK_MIDDLE); |
| #if AP_MODULE_MAGIC_AT_LEAST(20140207,2) |
| APR_OPTIONAL_HOOK(proxy, detach_backend, ssl_ct_detach_backend, NULL, NULL, |
| APR_HOOK_MIDDLE); |
| #endif |
| APR_OPTIONAL_HOOK(ssl, init_server, ssl_ct_init_server, NULL, NULL, |
| APR_HOOK_MIDDLE); |
| APR_OPTIONAL_HOOK(ssl, pre_handshake, |
| ssl_ct_pre_handshake, |
| NULL, NULL, APR_HOOK_MIDDLE); |
| APR_OPTIONAL_HOOK(ssl, proxy_post_handshake, ssl_ct_proxy_post_handshake, |
| NULL, NULL, APR_HOOK_MIDDLE); |
| } |
| |
| static const char *parse_num(apr_pool_t *p, |
| const char *arg, long min_val, |
| long max_val, long *val, |
| const char *cmd_name) |
| { |
| char *endptr; |
| |
| errno = 0; |
| *val = strtol(arg, &endptr, 10); |
| if (errno != 0 |
| || *endptr != '\0' |
| || *val < min_val |
| || *val > max_val) { |
| return apr_psprintf(p, "%s must be between %ld " |
| "and %ld (was '%s')", cmd_name, min_val, |
| max_val, arg); |
| } |
| |
| return NULL; |
| } |
| |
| static const char *ct_audit_storage(cmd_parms *cmd, void *x, const char *arg) |
| { |
| ct_server_config *sconf = ap_get_module_config(cmd->server->module_config, |
| &ssl_ct_module); |
| const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); |
| |
| if (err) { |
| return err; |
| } |
| |
| sconf->audit_storage = ap_runtime_dir_relative(cmd->pool, arg); |
| |
| if (!ctutil_dir_exists(cmd->temp_pool, sconf->audit_storage)) { |
| return apr_pstrcat(cmd->pool, "CTAuditStorage: Directory ", |
| sconf->audit_storage, |
| " does not exist", NULL); |
| } |
| |
| return NULL; |
| } |
| |
| static const char *ct_log_config_db(cmd_parms *cmd, void *x, const char *arg) |
| { |
| ct_server_config *sconf = ap_get_module_config(cmd->server->module_config, |
| &ssl_ct_module); |
| const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); |
| |
| if (err) { |
| return err; |
| } |
| |
| sconf->log_config_fname = ap_server_root_relative(cmd->pool, arg); |
| |
| return NULL; |
| } |
| |
| static const char *ct_max_sct_age(cmd_parms *cmd, void *x, const char *arg) |
| { |
| ct_server_config *sconf = ap_get_module_config(cmd->server->module_config, |
| &ssl_ct_module); |
| const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); |
| long val; |
| |
| if (err) { |
| return err; |
| } |
| |
| err = parse_num(cmd->pool, arg, 10, 3600 * 12, &val, "CTMaxSCTAge"); |
| if (err) { |
| return err; |
| } |
| |
| sconf->max_sct_age = apr_time_from_sec(val); |
| return NULL; |
| } |
| |
| static const char *ct_proxy_awareness(cmd_parms *cmd, void *x, const char *arg) |
| { |
| ct_server_config *sconf = ap_get_module_config(cmd->server->module_config, |
| &ssl_ct_module); |
| |
| if (!strcasecmp(arg, "oblivious")) { |
| sconf->proxy_awareness = PROXY_OBLIVIOUS; |
| } |
| else if (!strcasecmp(arg, "aware")) { |
| sconf->proxy_awareness = PROXY_AWARE; |
| } |
| else if (!strcasecmp(arg, "require")) { |
| sconf->proxy_awareness = PROXY_REQUIRE; |
| } |
| else { |
| return apr_pstrcat(cmd->pool, "CTProxyAwareness: Invalid argument \"", |
| arg, "\"", NULL); |
| } |
| |
| return NULL; |
| } |
| |
| static const char *ct_sct_storage(cmd_parms *cmd, void *x, const char *arg) |
| { |
| ct_server_config *sconf = ap_get_module_config(cmd->server->module_config, |
| &ssl_ct_module); |
| const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); |
| |
| if (err) { |
| return err; |
| } |
| |
| sconf->sct_storage = ap_runtime_dir_relative(cmd->pool, arg); |
| |
| if (!ctutil_dir_exists(cmd->temp_pool, sconf->sct_storage)) { |
| return apr_pstrcat(cmd->pool, "CTSCTStorage: Directory ", |
| sconf->sct_storage, |
| " does not exist", NULL); |
| } |
| |
| return NULL; |
| } |
| |
| static const char *ct_sct_limit(cmd_parms *cmd, void *x, const char *arg) |
| { |
| ct_server_config *sconf = ap_get_module_config(cmd->server->module_config, |
| &ssl_ct_module); |
| const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); |
| long val; |
| |
| if (err) { |
| return err; |
| } |
| |
| err = parse_num(cmd->pool, arg, 1, 100, &val, |
| "CTServerHelloSCTLimit"); |
| if (err) { |
| return err; |
| } |
| |
| sconf->max_sh_sct = val; |
| return NULL; |
| } |
| |
| static const char *ct_static_log_config(cmd_parms *cmd, void *x, int argc, |
| char *const argv[]) |
| { |
| apr_status_t rv; |
| const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); |
| const char *log_id, *public_key, *distrusted, *min_valid_time, |
| *max_valid_time, *url; |
| ct_server_config *sconf = ap_get_module_config(cmd->server->module_config, |
| &ssl_ct_module); |
| int cur_arg; |
| |
| if (err) { |
| return err; |
| } |
| |
| if (argc != 6) { |
| return "CTStaticLogConfig: 6 arguments are required"; |
| } |
| |
| cur_arg = 0; |
| log_id = argv[cur_arg++]; |
| if (!strcmp(log_id, "-")) { |
| log_id = NULL; |
| } |
| |
| public_key = argv[cur_arg++]; |
| if (!strcmp(public_key, "-")) { |
| public_key = NULL; |
| } |
| else { |
| public_key = ap_server_root_relative(cmd->pool, public_key); |
| } |
| |
| distrusted = argv[cur_arg++]; |
| if (!strcmp(distrusted, "-")) { |
| distrusted = NULL; |
| } |
| |
| min_valid_time = argv[cur_arg++]; |
| if (!strcmp(min_valid_time, "-")) { |
| min_valid_time = NULL; |
| } |
| |
| max_valid_time = argv[cur_arg++]; |
| if (!strcmp(max_valid_time, "-")) { |
| max_valid_time = NULL; |
| } |
| |
| url = argv[cur_arg++]; |
| if (!strcmp(url, "-")) { |
| url = NULL; |
| } |
| |
| if (!sconf->static_log_config) { |
| sconf->static_log_config = |
| apr_array_make(cmd->pool, 2, sizeof(ct_log_config *)); |
| } |
| rv = save_log_config_entry(sconf->static_log_config, cmd->pool, |
| log_id, public_key, distrusted, |
| min_valid_time, max_valid_time, url); |
| if (rv != APR_SUCCESS) { |
| return "Error processing static log configuration"; |
| } |
| |
| return NULL; |
| } |
| |
| static const char *ct_static_scts(cmd_parms *cmd, void *x, const char *cert_fn, |
| const char *sct_dn) |
| { |
| apr_pool_t *p = cmd->pool; |
| apr_status_t rv; |
| ct_server_config *sconf = ap_get_module_config(cmd->server->module_config, |
| &ssl_ct_module); |
| const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); |
| const char *fingerprint; |
| FILE *pemfile; |
| X509 *cert; |
| |
| if (err) { |
| return err; |
| } |
| |
| cert_fn = ap_server_root_relative(p, cert_fn); |
| sct_dn = ap_server_root_relative(p, sct_dn); |
| |
| rv = ctutil_fopen(cert_fn, "r", &pemfile); |
| if (rv != APR_SUCCESS) { |
| return apr_psprintf(p, "could not open certificate file %s (%pm)", |
| cert_fn, &rv); |
| } |
| |
| cert = PEM_read_X509(pemfile, NULL, NULL, NULL); |
| fclose(pemfile); |
| |
| if (!cert) { |
| return apr_psprintf(p, "could not read certificate from file %s", |
| cert_fn); |
| } |
| |
| fingerprint = get_cert_fingerprint(p, cert); |
| X509_free(cert); |
| |
| if (!ctutil_dir_exists(cmd->temp_pool, sct_dn)) { |
| return apr_pstrcat(p, "CTStaticSCTs: Directory ", sct_dn, |
| " does not exist", NULL); |
| } |
| |
| apr_hash_set(sconf->static_cert_sct_dirs, fingerprint, |
| APR_HASH_KEY_STRING, sct_dn); |
| |
| return NULL; |
| } |
| |
| static const char *ct_log_client(cmd_parms *cmd, void *x, const char *arg) |
| { |
| ct_server_config *sconf = ap_get_module_config(cmd->server->module_config, |
| &ssl_ct_module); |
| const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); |
| |
| if (err) { |
| return err; |
| } |
| |
| if (strcmp(DOTEXE, "")) { |
| if (!ctutil_file_exists(cmd->temp_pool, arg)) { |
| arg = apr_pstrcat(cmd->pool, arg, DOTEXE, NULL); |
| } |
| } |
| |
| if (!ctutil_file_exists(cmd->temp_pool, arg)) { |
| return apr_pstrcat(cmd->pool, |
| "CTLogClient: File ", |
| arg, |
| " does not exist", |
| NULL); |
| } |
| |
| sconf->ct_exe = arg; |
| |
| return NULL; |
| } |
| |
| static const command_rec ct_cmds[] = |
| { |
| AP_INIT_TAKE1("CTAuditStorage", ct_audit_storage, NULL, |
| RSRC_CONF, /* GLOBAL_ONLY - audit data spans servers */ |
| "Location to store files of audit data"), |
| AP_INIT_TAKE1("CTLogConfigDB", ct_log_config_db, NULL, |
| RSRC_CONF, /* GLOBAL_ONLY - otherwise, you couldn't share |
| * the same SCT list for a cert used by two |
| * different vhosts (and the SCT maintenance daemon |
| * would be more complex) |
| */ |
| "Log configuration database"), |
| AP_INIT_TAKE1("CTMaxSCTAge", ct_max_sct_age, NULL, |
| RSRC_CONF, /* GLOBAL_ONLY - otherwise, you couldn't share |
| * the same SCT list for a cert used by two |
| * different vhosts |
| */ |
| "Max age of SCT obtained from log before refresh"), |
| AP_INIT_TAKE1("CTProxyAwareness", ct_proxy_awareness, NULL, |
| RSRC_CONF, /* per-server */ |
| "\"oblivious\" to neither ask for nor check SCTs, " |
| "\"aware\" to ask for and process SCTs but allow all connections, " |
| "or \"require\" to abort backend connections if an acceptable " |
| "SCT is not provided"), |
| AP_INIT_TAKE1("CTServerHelloSCTLimit", ct_sct_limit, NULL, |
| RSRC_CONF, /* GLOBAL_ONLY - otherwise, you couldn't share |
| * the same SCT list for a cert used by two |
| * different vhosts |
| */ |
| "Limit on number of SCTs sent in ServerHello"), |
| AP_INIT_TAKE1("CTSCTStorage", ct_sct_storage, NULL, |
| RSRC_CONF, /* GLOBAL_ONLY - otherwise, you couldn't share |
| * the same SCT list for a cert used by two |
| * different vhosts (and the SCT maintenance daemon |
| * would be more complex) |
| */ |
| "Location to store SCTs obtained from logs"), |
| AP_INIT_TAKE_ARGV("CTStaticLogConfig", ct_static_log_config, NULL, |
| RSRC_CONF, /* GLOBAL_ONLY */ |
| "Static log configuration record"), |
| AP_INIT_TAKE2("CTStaticSCTs", ct_static_scts, NULL, |
| RSRC_CONF, /* GLOBAL_ONLY - otherwise, you couldn't share |
| * the same SCT list for a cert used by two |
| * different vhosts (and the SCT maintenance daemon |
| * would be more complex) |
| */ |
| "Point to directory with static SCTs corresponding to the " |
| "specified certificate"), |
| AP_INIT_TAKE1("CTLogClient", ct_log_client, NULL, |
| RSRC_CONF, /* GLOBAL_ONLY - otherwise, you couldn't share |
| * the same SCTs for a cert used by two |
| * different vhosts (and it would be just plain |
| * silly :) ) |
| */ |
| "Location of certificate-transparency.org (or compatible) log client tool"), |
| {NULL} |
| }; |
| |
| AP_DECLARE_MODULE(ssl_ct) = |
| { |
| STANDARD20_MODULE_STUFF, |
| NULL, |
| NULL, |
| create_ct_server_config, |
| merge_ct_server_config, |
| ct_cmds, |
| ct_register_hooks, |
| }; |