/* Licensed to Stichting The Commons Conservancy (TCC) under one or more * contributor license agreements. See the AUTHORS file distributed with * this work for additional information regarding copyright ownership. * TCC 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. */ /* * Generate and return CRLs backed by mod_ca. * * Author: Graham Leggett * */ #include #include #include #include #include #include #include #include #include #include "httpd.h" #include "http_config.h" #include "http_core.h" #include "http_log.h" #include "http_protocol.h" #include "http_request.h" #include "util_script.h" #include "mod_ca.h" module AP_MODULE_DECLARE_DATA crl_module; typedef enum { ENCODING_DER, ENCODING_PEM, ENCODING_XPEM } encoding_t; #define DEFAULT_CRL_ENCODING ENCODING_DER #define DEFAULT_FRESHNESS 2 #define DEFAULT_FRESHNESS_MAX 3600*24 typedef struct { encoding_t encoding; int encoding_set; int freshness; int freshness_max; int freshness_set; const char *location; int location_set; } crl_config_rec; static void *create_crl_dir_config(apr_pool_t *p, char *d) { crl_config_rec *conf = apr_pcalloc(p, sizeof(crl_config_rec)); conf->encoding = DEFAULT_CRL_ENCODING; conf->freshness = DEFAULT_FRESHNESS; conf->freshness_max = DEFAULT_FRESHNESS_MAX; return conf; } static void *merge_crl_dir_config(apr_pool_t *p, void *basev, void *addv) { crl_config_rec *new = (crl_config_rec *) apr_pcalloc(p, sizeof(crl_config_rec)); crl_config_rec *add = (crl_config_rec *) addv; crl_config_rec *base = (crl_config_rec *) basev; new->encoding = (add->encoding_set == 0) ? base->encoding : add->encoding; new->encoding_set = add->encoding_set || base->encoding_set; new->freshness = (add->freshness_set == 0) ? base->freshness : add->freshness; new->freshness_max = (add->freshness_set == 0) ? base->freshness_max : add->freshness_max; new->freshness_set = add->freshness_set || base->freshness_set; new->location = (add->location_set == 0) ? base->location : add->location; new->location_set = add->location_set || base->location_set; return new; } static const char *set_crl_encoding(cmd_parms *cmd, void *dconf, const char *arg) { crl_config_rec *conf = dconf; if (!strcmp(arg, "der")) { conf->encoding = ENCODING_DER; } else if (!strcmp(arg, "pem")) { conf->encoding = ENCODING_PEM; } else if (!strcmp(arg, "x-pem")) { conf->encoding = ENCODING_XPEM; } else { return apr_psprintf(cmd->pool, "The encoding '%s' wasn't 'pem', 'x-pem' or 'der'.", arg); } conf->encoding_set = 1; return NULL; } static const char *set_crl_freshness(cmd_parms *cmd, void *dconf, const char *arg, const char *max) { crl_config_rec *conf = dconf; conf->freshness = atoi(arg); if (max) { conf->freshness_max = atoi(max); } conf->freshness_set = 1; if (conf->freshness < 0 || conf->freshness_max < 0) { return "CRLFreshness must specify a positive integer (or integers)"; } return NULL; } static const char *set_location(cmd_parms *cmd, void *dconf, const char *arg) { crl_config_rec *conf = dconf; conf->location = arg; conf->location_set = 1; return NULL; } static const command_rec crl_cmds[] = { AP_INIT_TAKE1("CrlEncoding", set_crl_encoding, NULL, RSRC_CONF | ACCESS_CONF, "Set to the default encoding to be returned if not specified. Must be \"pem\", \"x-pem\" or \"der\". Defaults to \"der\"."), AP_INIT_TAKE12("CrlFreshness", set_crl_freshness, NULL, RSRC_CONF | ACCESS_CONF, "The age of the CRL will be divided by this factor when added as a max-age, set to zero to disable. Defaults to \"2\". An optional maximum value can be specified, defaults to one day."), AP_INIT_TAKE1("CrlLocation", set_location, NULL, RSRC_CONF | ACCESS_CONF, "Set to the location of the CRL service."), { NULL } }; static void log_message(request_rec *r, apr_status_t status, const char *message) { int len; BIO *mem = BIO_new(BIO_s_mem()); char *err = apr_palloc(r->pool, HUGE_STRING_LEN); ERR_print_errors(mem); len = BIO_gets(mem, err, HUGE_STRING_LEN - 1); if (len > -1) { err[len] = 0; } apr_table_setn(r->notes, "error-notes", apr_pstrcat(r->pool, "CRL could not be returned: ", ap_escape_html( r->pool, message), NULL)); /* Allow "error-notes" string to be printed by ap_send_error_response() */ apr_table_setn(r->notes, "verbose-error-to", "*"); if (len > 0) { ap_log_rerror( APLOG_MARK, APLOG_ERR, status, r, "%s (%s)", message, err); } else { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, "%s", message); } BIO_free(mem); } static apr_status_t crl_BIO_cleanup(void *data) { BIO_free((BIO *) data); return APR_SUCCESS; } static apr_status_t crl_X509_CRL_cleanup(void *data) { X509_CRL_free((X509_CRL *) data); return APR_SUCCESS; } static int get_crl(request_rec *r, crl_config_rec *conf) { apr_size_t len; apr_off_t offset; const unsigned char *der; encoding_t encoding; int rv; apr_bucket_brigade *bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); apr_bucket *e; apr_status_t status; apr_sha1_ctx_t sha1; apr_byte_t digest[APR_SHA1_DIGESTSIZE]; char *etag; apr_time_t validity; const char *accept_encoding = apr_table_get(r->headers_in, "Accept-Encoding"); const char *vary = apr_table_get(r->headers_out, "Vary"); /* discard the request body */ if ((rv = ap_discard_request_body(r)) != OK) { return rv; } /* get the crl */ rv = ap_run_ca_getcrl(r, &der, &len, &validity); if (rv == DECLINED) { log_message(r, APR_SUCCESS, "No module configured to return the certificate revocation list"); return HTTP_INTERNAL_SERVER_ERROR; } if (rv > OK) { return rv; } /* what content encoding have we been asked for? */ if (!accept_encoding) { encoding = conf->encoding; } else { char *last, *token, *value; if (!vary) { apr_table_setn(r->headers_out, "Vary", "Accept-Encoding"); } else { if (!ap_find_list_item(r->pool, vary, "encoding")) { apr_table_setn(r->headers_out, "Vary", apr_pstrcat(r->pool, vary, ",", "Accept-Encoding", NULL)); } } token = apr_strtok(apr_pstrdup(r->pool, accept_encoding), ",", &last); while (token) { char *param = strchr(token, ';'); if (param) { value = apr_pstrndup(r->pool, token, param - token); } else { value = token; } if (!strcmp(value, "identity")) { encoding = ENCODING_DER; } else if (!strcmp(value, "pem")) { encoding = ENCODING_PEM; } else if (!strcmp(value, "x-pem")) { encoding = ENCODING_XPEM; } token = apr_strtok(NULL, ",", &last); } } /* handle delivery */ apr_sha1_init(&sha1); switch (encoding) { case ENCODING_PEM: case ENCODING_XPEM: { char buf[APR_BUCKET_BUFF_SIZE]; X509_CRL *crl; BIO *out = BIO_new(BIO_s_mem()); apr_pool_cleanup_register(r->pool, out, crl_BIO_cleanup, apr_pool_cleanup_null); crl = d2i_X509_CRL(NULL, &der, len); if (!crl) { log_message(r, APR_SUCCESS, "CRL returned could not be parsed"); return HTTP_INTERNAL_SERVER_ERROR; } apr_pool_cleanup_register(r->pool, crl, crl_X509_CRL_cleanup, apr_pool_cleanup_null); if (!X509_CRL_print(out, crl)) { log_message(r, APR_SUCCESS, "CRL summary could not be printed"); return HTTP_INTERNAL_SERVER_ERROR; } if (!PEM_write_bio_X509_CRL(out, crl)) { log_message(r, APR_SUCCESS, "CRL could not be PEM encoded"); return HTTP_INTERNAL_SERVER_ERROR; } ap_set_content_type(r, "application/pkix-crl"); apr_table_setn(r->headers_out, "Content-Encoding", encoding == ENCODING_PEM ? "pem" : "x-pem"); ap_set_content_length(r, BIO_ctrl_pending(out)); while ((offset = BIO_read(out, buf, sizeof(buf))) > 0) { apr_sha1_update(&sha1, buf, offset); apr_brigade_write(bb, NULL, NULL, buf, offset); } break; } case ENCODING_DER: { ap_set_content_type(r, "application/pkix-crl"); apr_sha1_update_binary(&sha1, der, len); ap_set_content_length(r, len); e = apr_bucket_pool_create((const char *) der, len, r->pool, r->connection->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, e); break; } } apr_sha1_final(digest, &sha1); etag = apr_palloc(r->pool, 31); apr_base64_encode_binary(etag + 1, digest, sizeof(digest)); etag[0] = '\"'; etag[29] = '\"'; etag[30] = 0; apr_table_setn(r->headers_out, "ETag", etag); /* handle freshness lifetime for caching */ if (!apr_table_get(r->headers_out, "Cache-Control")) { apr_off_t delta = apr_time_sec(validity - apr_time_now()); delta = delta > 0 ? conf->freshness ? delta / conf->freshness : 0 : 0; delta = delta < conf->freshness_max ? delta : conf->freshness_max; apr_table_setn(r->headers_out, "Cache-Control", apr_psprintf(r->pool, "max-age=%" APR_OFF_T_FMT, delta)); } if ((rv = ap_meets_conditions(r)) != OK) { r->status = rv; apr_brigade_cleanup(bb); } else { apr_brigade_length(bb, 1, &offset); len = offset; } e = apr_bucket_eos_create(r->connection->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, e); status = ap_pass_brigade(r->output_filters, bb); if (status == APR_SUCCESS || r->status != HTTP_OK || r->connection->aborted) { return OK; } else { /* no way to know what type of error occurred */ ap_log_rerror( APLOG_MARK, APLOG_DEBUG, status, r, "crl_handler: ap_pass_brigade returned %i", status); return HTTP_INTERNAL_SERVER_ERROR; } /* ready to leave */ return OK; } static int options_wadl(request_rec *r, crl_config_rec *conf) { int rv; /* discard the request body */ if ((rv = ap_discard_request_body(r)) != OK) { return rv; } ap_set_content_type(r, "application/vnd.sun.wadl+xml"); ap_rprintf(r, "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " On a configuration error, 500 Internal Server Error will be returned,\n" " and the server error log will contain full details of the\n" " error.\n" " \n" " \n" " \n" " \n" " If the ETag specified within the If-None-Match header is unmodified\n" " compared to the current ETag, 304 Not Modified is returned with no body..\n" " \n" " \n" " \n" " \n" " After a successful signing of the certificate, 200 OK will be returned\n" " with the body containing the ASN.1 DER-encoded X509 certificate.\n" " \n" " \n" " \n" " \n" " \n" "\n", conf->location ? conf->location : apr_pstrcat(r->pool, ap_http_scheme(r), "://", r->server->server_hostname, r->uri, NULL)); return OK; } static int crl_handler(request_rec *r) { crl_config_rec *conf = ap_get_module_config(r->per_dir_config, &crl_module); if (!conf) { return DECLINED; } if (strcmp(r->handler, "crl")) { return DECLINED; } /* A GET should return the CRL, OPTIONS should return the WADL */ ap_allow_methods(r, 1, "GET", "OPTIONS", NULL); if (!strcmp(r->method, "GET")) { return get_crl(r, conf); } else if (!strcmp(r->method, "OPTIONS")) { return options_wadl(r, conf); } else { return HTTP_METHOD_NOT_ALLOWED; } } static apr_status_t crl_cleanup(void *data) { ERR_free_strings(); EVP_cleanup(); return APR_SUCCESS; } static int crl_pre_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp) { OpenSSL_add_all_algorithms(); ERR_load_crypto_strings(); apr_pool_cleanup_register(pconf, NULL, crl_cleanup, apr_pool_cleanup_null); return APR_SUCCESS; } static void register_hooks(apr_pool_t *p) { ap_hook_pre_config(crl_pre_config, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_handler(crl_handler, NULL, NULL, APR_HOOK_MIDDLE); } module AP_MODULE_DECLARE_DATA crl_module = { STANDARD20_MODULE_STUFF, create_crl_dir_config, /* dir config creater */ merge_crl_dir_config, /* dir merger --- default is to override */ NULL, /* server config */ NULL, /* merge server config */ crl_cmds, /* command apr_table_t */ register_hooks /* register hooks */ };