/* 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 certificates 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 pkcs7_module; typedef enum { ENCODING_DER, ENCODING_PEM, ENCODING_XPEM } encoding_t; #define DEFAULT_CERT_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; } cert_config_rec; static void *create_pkcs7_dir_config(apr_pool_t *p, char *d) { cert_config_rec *conf = apr_pcalloc(p, sizeof(cert_config_rec)); conf->encoding = DEFAULT_CERT_ENCODING; conf->freshness = DEFAULT_FRESHNESS; conf->freshness_max = DEFAULT_FRESHNESS_MAX; return conf; } static void *merge_pkcs7_dir_config(apr_pool_t *p, void *basev, void *addv) { cert_config_rec *new = (cert_config_rec *) apr_pcalloc(p, sizeof(cert_config_rec)); cert_config_rec *add = (cert_config_rec *) addv; cert_config_rec *base = (cert_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_pkcs7_encoding(cmd_parms *cmd, void *dconf, const char *arg) { cert_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_pkcs7_freshness(cmd_parms *cmd, void *dconf, const char *arg, const char *max) { cert_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 "CertFreshness must specify a positive integer (or integers)"; } return NULL; } static const char *set_location(cmd_parms *cmd, void *dconf, const char *arg) { cert_config_rec *conf = dconf; conf->location = arg; conf->location_set = 1; return NULL; } static const command_rec pkcs7_cmds[] = { AP_INIT_TAKE1("Pkcs7Encoding", set_pkcs7_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("Pkcs7Freshness", set_pkcs7_freshness, NULL, RSRC_CONF | ACCESS_CONF, "The age of the certificate 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("Pkcs7Location", set_location, NULL, RSRC_CONF | ACCESS_CONF, "Set to the location of the certificate 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, "Certificate 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 pkcs7_BIO_cleanup(void *data) { BIO_free((BIO *) data); return APR_SUCCESS; } static apr_status_t pkcs7_X509_cleanup(void *data) { X509_free((X509 *) data); return APR_SUCCESS; } static apr_status_t pkcs7_PKCS7_cleanup(void *data) { PKCS7_free((PKCS7 *) data); return APR_SUCCESS; } static encoding_t detect_encoding(request_rec *r) { cert_config_rec *conf = ap_get_module_config(r->per_dir_config, &pkcs7_module); encoding_t encoding = conf->encoding; const char *accept_encoding = apr_table_get(r->headers_in, "Accept-Encoding"); const char *vary = apr_table_get(r->headers_out, "Vary"); /* 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); } } return encoding; } static int get_pkcs7(request_rec *r, const unsigned char *der, apr_size_t len, apr_time_t validity) { apr_sha1_ctx_t sha1; apr_byte_t digest[APR_SHA1_DIGESTSIZE]; apr_bucket_brigade *bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); apr_bucket *e; char *etag; const unsigned char *tmp; PKCS7 *p7 = NULL; X509 *cert = NULL; cert_config_rec *conf = ap_get_module_config(r->per_dir_config, &pkcs7_module); apr_off_t offset; apr_status_t status; int rv; encoding_t encoding; /* discard the request body */ if ((rv = ap_discard_request_body(r)) != OK) { return rv; } /* create a new signed data PKCS#7 */ p7 = PKCS7_new(); if (!p7) { log_message(r, APR_SUCCESS, "could not create a PKCS7 degenerate response"); return HTTP_INTERNAL_SERVER_ERROR; } else { apr_pool_cleanup_register(r->pool, p7, pkcs7_PKCS7_cleanup, apr_pool_cleanup_null); } PKCS7_set_type(p7, NID_pkcs7_signed); PKCS7_content_new(p7, NID_pkcs7_data); tmp = der; if (!d2i_X509(&cert, &tmp, len)) { log_message(r, APR_SUCCESS, "could not DER decode the CA certificate"); return HTTP_INTERNAL_SERVER_ERROR; } apr_pool_cleanup_register(r->pool, cert, pkcs7_X509_cleanup, apr_pool_cleanup_null); if (!PKCS7_add_certificate(p7, cert)) { log_message(r, APR_SUCCESS, "could not add the CA certificate to the degenerate PKCS7 response"); return HTTP_INTERNAL_SERVER_ERROR; } encoding = detect_encoding(r); /* handle delivery */ apr_sha1_init(&sha1); switch (encoding) { case ENCODING_PEM: case ENCODING_XPEM: { char buf[APR_BUCKET_BUFF_SIZE]; /* write out the PEM encoded pkcs7 structure */ BIO *out = BIO_new(BIO_s_mem()); apr_pool_cleanup_register(r->pool, out, pkcs7_BIO_cleanup, apr_pool_cleanup_null); if (!PEM_write_bio_PKCS7(out, p7)) { log_message(r, APR_SUCCESS, "could not PEM encode the PKCS7 certificate response"); return HTTP_INTERNAL_SERVER_ERROR; } /* content type */ ap_set_content_type(r, "application/pkcs7-mime"); apr_table_set(r->headers_out, "Content-Disposition", "inline, filename=ca-cert.p7b"); 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/x-pkcs7-certificates"); 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, "pkcs7_handler: ap_pass_brigade returned %i", status); return HTTP_INTERNAL_SERVER_ERROR; } /* ready to leave */ return OK; } static int options_wadl(request_rec *r, cert_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" " When the certificate is available, 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 pkcs7_getca_handler(request_rec *r) { cert_config_rec *conf = ap_get_module_config(r->per_dir_config, &pkcs7_module); if (!conf || !r->handler || r->handler[0] != 'p' || strcmp(r->handler, "pkcs7-ca")) { return DECLINED; } /* A GET should return the certificates, OPTIONS should return the WADL */ ap_allow_methods(r, 1, "GET", "OPTIONS", NULL); if (!strcmp(r->method, "GET")) { apr_size_t len; const unsigned char *der; apr_time_t validity; int rv; /* get the ca certificate */ rv = ap_run_ca_getca(r, &der, &len, &validity); if (rv == DECLINED) { log_message(r, APR_SUCCESS, "No module configured to return the CA certificate"); return HTTP_NOT_FOUND; } if (rv > OK) { return rv; } if (!len) { log_message(r, APR_SUCCESS, "No CA certificate is available"); return HTTP_NOT_FOUND; } return get_pkcs7(r, der, len, validity); } else if (!strcmp(r->method, "OPTIONS")) { return options_wadl(r, conf); } else { return HTTP_METHOD_NOT_ALLOWED; } } static int pkcs7_getnextca_handler(request_rec *r) { cert_config_rec *conf = ap_get_module_config(r->per_dir_config, &pkcs7_module); if (!conf || !r->handler || r->handler[0] != 'p' || strcmp(r->handler, "pkcs7-nextca")) { return DECLINED; } /* A GET should return the certificates, OPTIONS should return the WADL */ ap_allow_methods(r, 1, "GET", "OPTIONS", NULL); if (!strcmp(r->method, "GET")) { apr_size_t len; const unsigned char *der; apr_time_t validity; int rv; /* get the next ca certificate */ rv = ap_run_ca_getnextca(r, &der, &len, &validity); if (rv == DECLINED) { log_message(r, APR_SUCCESS, "No module configured to return the next CA certificate"); return HTTP_NOT_FOUND; } if (rv > OK) { return rv; } if (!len) { log_message(r, APR_SUCCESS, "No next CA certificate is available"); return HTTP_NOT_FOUND; } return get_pkcs7(r, der, len, validity); } else if (!strcmp(r->method, "OPTIONS")) { return options_wadl(r, conf); } else { return HTTP_METHOD_NOT_ALLOWED; } } static apr_status_t pkcs7_cleanup(void *data) { ERR_free_strings(); EVP_cleanup(); return APR_SUCCESS; } static int pkcs7_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, pkcs7_cleanup, apr_pool_cleanup_null); return APR_SUCCESS; } static void register_hooks(apr_pool_t *p) { ap_hook_pre_config(pkcs7_pre_config, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_handler(pkcs7_getca_handler, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_handler(pkcs7_getnextca_handler, NULL, NULL, APR_HOOK_MIDDLE); } AP_DECLARE_MODULE(pkcs7) = { STANDARD20_MODULE_STUFF, create_pkcs7_dir_config, /* dir config creater */ merge_pkcs7_dir_config, /* dir merger --- default is to override */ NULL, /* server config */ NULL, /* merge server config */ pkcs7_cmds, /* command apr_table_t */ register_hooks /* register hooks */ };