| /* 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_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_pool_t *service_pool; | |
| 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; | |
| return apr_thread_join(&retval, thd); | |
| } | |
| static apr_status_t stop_service_thread(void *data) | |
| { | |
| /* the apr_thread_t lives in service_pool */ | |
| if (wait_for_thread(data) == APR_SUCCESS) { | |
| apr_pool_destroy(service_pool); | |
| } | |
| service_thread = NULL; | |
| service_pool = NULL; | |
| 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; | |
| unsigned char digest[SHA256_DIGEST_LENGTH]; | |
| fp = get_cert_fingerprint(c->pool, cc->leaf); | |
| #if OPENSSL_VERSION_NUMBER < 0x30000000L | |
| { | |
| SHA256_CTX sha256ctx; | |
| 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 */ | |
| } | |
| #else | |
| { | |
| EVP_MD_CTX *md_ctx; | |
| unsigned int dlen = 0; | |
| md_ctx = EVP_MD_CTX_create(); | |
| ap_assert(md_ctx != NULL); | |
| ap_assert(EVP_DigestInit_ex(md_ctx, EVP_sha256(), NULL)); | |
| ap_assert(EVP_DigestUpdate(md_ctx, (unsigned char *)fp, strlen(fp))); | |
| if (conncfg->cert_sct_list) { | |
| ap_assert(EVP_DigestUpdate(md_ctx, conncfg->cert_sct_list, | |
| conncfg->cert_sct_list_size)); | |
| } | |
| if (conncfg->serverhello_sct_list) { | |
| ap_assert(EVP_DigestUpdate(md_ctx, conncfg->serverhello_sct_list, | |
| conncfg->serverhello_sct_list_size)); | |
| } | |
| if (conncfg->ocsp_sct_list) { | |
| ap_assert(EVP_DigestUpdate(md_ctx, conncfg->ocsp_sct_list, | |
| conncfg->ocsp_sct_list_size)); | |
| } | |
| ap_assert(EVP_DigestFinal_ex(md_ctx, digest, &dlen)); | |
| ap_assert(dlen == SHA256_DIGEST_LENGTH); | |
| EVP_MD_CTX_destroy(md_ctx); | |
| } | |
| #endif | |
| 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 %u 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 %u 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); | |
| } | |
| /* Not a subpool of pchild: with APR pool-debug, destroying pchild holds | |
| * its pool-tree mutex while running the pre-cleanup that joins the | |
| * thread, and apr_thread_exit() needs the mutex of the thread pool's | |
| * parent. Some MPM exit paths (prefork after a signal) destroy pchild | |
| * without running child_stopped, so the join must work from there. | |
| */ | |
| rv = apr_pool_create_unmanaged_ex(&service_pool, apr_pool_abort_get(p), | |
| NULL); | |
| if (rv == APR_SUCCESS) { | |
| apr_pool_tag(service_pool, "ssl_ct_service"); | |
| rv = ap_thread_create(&service_thread, NULL, run_service_thread, s, | |
| service_pool); | |
| } | |
| 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, stop_service_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, | |
| }; |