/* 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. */ /* CMS Verify of any post input * * AddHandler cgi-script .cgi * * SetInputFilter cmsverify * CMSVerifyCertificate ..../ca.cert (or a specific whitelisted leaf certificate) * * # ProxyPass https://127.0.0.1/api * # Alias ... etc. * * * where this Location is then routed to a ProxyPass or handled by * handled by cgi, php, etc. * * This file was written during the Dutch COVID response at the * ministry of Public Health of the Netherlands; to verify that * a key security mechanism used in the citizen app was working * as deverifyed. */ #include #include #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 "apr_general.h" #include "util_filter.h" #define DEFAULT_MD (NID_sha256) #define HANDLER "cmsverify" #define MAX_PKCS7_SIZE (128 * 1024) #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) module AP_MODULE_DECLARE_DATA cms_verify_module; typedef enum { OF_DER = 0, OF_JSON } verify_input_type; typedef struct { const char *location; apr_size_t max_size; X509_STORE *trusted_certs; STACK_OF(X509) * other_certs; verify_input_type tp; } verify_config_rec; typedef struct { apr_bucket_brigade *pbb_tmp; BIO *in; apr_size_t lenin; } cms_filter_state; static apr_status_t _cleanup(void *data) { ERR_free_strings(); EVP_cleanup(); return APR_SUCCESS; } static apr_status_t verify_config_rec_cleanup(void *data) { verify_config_rec *rec = (verify_config_rec *) data; X509_STORE_free(rec->trusted_certs); sk_X509_pop_free(rec->other_certs, X509_free); return APR_SUCCESS; } static apr_status_t state_cleanup(void *data) { cms_filter_state *state = (cms_filter_state *) data; if (state) { BIO_free(state->in); }; return APR_SUCCESS; } static void *_create_dir_config(apr_pool_t * p, char *dir) { verify_config_rec *conf = apr_pcalloc(p, sizeof(verify_config_rec)); conf->tp = OF_DER; conf->location = apr_pstrdup(p, dir); conf->max_size = MAX_PKCS7_SIZE; if ( ((conf->other_certs = sk_X509_new(NULL)) == NULL) || ((conf->trusted_certs = X509_STORE_new()) == NULL) || ((X509_STORE_set_purpose(conf->trusted_certs, X509_PURPOSE_ANY)) != 1) ) { ap_log_perror(APLOG_MARK, APLOG_ERR, 0, p, HANDLER ": out of memory"); return NULL; }; apr_pool_cleanup_register(p, conf, verify_config_rec_cleanup, apr_pool_cleanup_null); return conf; } static void _merge_sk_X509(STACK_OF(X509) * dst, STACK_OF(X509) * src) { for (int i = 0; i < sk_X509_num(src); i++) sk_X509_push(dst, sk_X509_value(src, i)); } static void _merge_X509_STORE(X509_STORE * dst, X509_STORE * src) { STACK_OF(X509_OBJECT) * objs = X509_STORE_get0_objects(src); for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) { X509 *crt = X509_OBJECT_get0_X509(sk_X509_OBJECT_value(objs, i)); X509_STORE_add_cert(dst, crt); }; } static void *_merge_dir_config(apr_pool_t * p, void *basev, void *addv) { verify_config_rec *new = _create_dir_config(p, NULL); verify_config_rec *add = (verify_config_rec *) addv; verify_config_rec *base = (verify_config_rec *) basev; _merge_X509_STORE(new->trusted_certs, base->trusted_certs); _merge_X509_STORE(new->trusted_certs, add->trusted_certs); _merge_sk_X509(new->other_certs, base->other_certs); _merge_sk_X509(new->other_certs, add->other_certs); return new; } static const char *set_max_size(cmd_parms *cmd, void *dconf, const char *arg) { verify_config_rec *conf = dconf; conf->max_size = apr_atoi64(arg); if (errno || conf->max_size <= 0) return "Invalid max value"; return NULL; } static const char *add_trusted_certs(cmd_parms *cmd, void *dconf, const char *arg) { verify_config_rec *conf = dconf; arg = ap_server_root_relative(cmd->pool, arg); X509_STORE *extras = X509_STORE_new(); if (1 != X509_STORE_load_locations(extras, arg, NULL)) { X509_STORE_free(extras); return apr_psprintf(cmd->pool, "Could not load trusted certfile: %s", arg); } _merge_X509_STORE(conf->trusted_certs, extras); X509_STORE_free(extras); return NULL; } static const char *add_untrusted_certs(cmd_parms *cmd, void *dconf, const char *arg) { verify_config_rec *conf = dconf; arg = ap_server_root_relative(cmd->pool, arg); X509_STORE *extras = X509_STORE_new(); if (1 != X509_STORE_load_locations(extras, arg, NULL)) { X509_STORE_free(extras); return apr_psprintf(cmd->pool, "Could not load untrusted certfile: %s", arg); } STACK_OF(X509_OBJECT) * objs = X509_STORE_get0_objects(extras); for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) { X509 *crt = X509_OBJECT_get0_X509(sk_X509_OBJECT_value(objs, i)); sk_X509_push(conf->other_certs, crt); }; return NULL; } static char * _extract_json(apr_pool_t *p, const char * data, apr_size_t len, const unsigned char ** signature, apr_size_t * signature_len, const unsigned char ** payload, apr_size_t * payload_len ) { const unsigned char ** dst = NULL; const char * ptr = data; const char * end = data + len; apr_size_t n, *dst_len; *signature = NULL; *payload = NULL; *signature_len = 0; *payload_len = 0; for(;ptr < end && isspace(*ptr); ptr++) {}; if (*ptr++ != '{') return "No starting {"; while(!*signature || !*payload) { for(;ptr < end && (isspace(*ptr) || *ptr ==','); ptr++,len--) {}; if (end - ptr < 12) return "Not enough data"; if (strncmp("\"signature\"",ptr,11) == 0) { ptr += 11; dst = signature; dst_len = signature_len; } else if (strncmp("\"payload\"",ptr,9) == 0) { ptr += 9; dst = payload; dst_len = payload_len; } else { return "Not some dict we know about"; }; for(;ptr < end && isspace(*ptr); ptr++,len--) {}; if (*ptr++ != ':') return "No value for dict entry"; for(;ptr < end && isspace(*ptr); ptr++,len--) {}; if (*ptr++ != '"') return "No start to the value"; for(n=0;ptr+n < end && ptr[n] != '"'; n++) { char c = ptr[n]; if (!isdigit(c) && !isalpha(c) && !isspace(c) && c != '+' && c != '/' && c != '=' && c != '_' && c != '-') return "Illegal base64 char"; }; if (ptr >= end) return "No end to the value"; if (((*dst = apr_palloc(p, apr_base64_decode_len(ptr))) == NULL) || ((*dst_len = apr_base64_decode_binary ((unsigned char *)*dst,ptr)) <= 0) ) return "Could not decode base64"; ptr += n + 1; }; return NULL; } static apr_bucket *_verify(request_rec *r, BIO * in) { verify_config_rec *conf = ap_get_module_config(r->per_dir_config, &cms_verify_module); STACK_OF(X509) * signers = NULL; X509_NAME *subject = NULL; CMS_ContentInfo *ci = NULL; apr_bucket *ret = NULL; X509 *signer = NULL; char *dn = NULL; BIO *out = NULL; const char *ptr = NULL; const unsigned char * sig, *pay; apr_size_t lsig, lpay; long len = 0; int flags = 0; const char * err = NULL; len = BIO_get_mem_data(in, &ptr); if (NULL == (err = _extract_json(r->pool, ptr, len, &sig, &lsig, &pay, &lpay))) { if (!(ci = d2i_CMS_ContentInfo(NULL, &sig, lsig))) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, HANDLER ": could not decode JSON: %s", ERR_reason_error_string(ERR_get_error())); goto exit; }; len = lpay; ptr = (const char *)pay; ptr = apr_pmemdup(r->pool, ptr, len); flags |= CMS_DETACHED; BIO_reset(in); BIO_write(in, ptr, len); out = NULL; } else if (!(out = BIO_new(BIO_s_mem())) || !(ci = d2i_CMS_bio(in, NULL))) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, HANDLER ": could not decode DER: %s", ERR_reason_error_string(ERR_get_error())); goto exit; } else { ptr = NULL; BIO_reset(in); }; if (1 != CMS_verify(ci, conf->other_certs, conf->trusted_certs, (out == NULL) ? in : NULL, out, flags)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, HANDLER ": signature check failed: %s", ERR_reason_error_string(ERR_get_error())); goto exit; } if (!(signers = CMS_get0_signers(ci)) || !(signer = sk_X509_value(signers, 0)) || !(subject = X509_get_subject_name(signer)) || !(dn = X509_NAME_oneline(subject, NULL, 0)) ) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, HANDLER ": verified; but no signers ?!: %s", ERR_reason_error_string(ERR_get_error())); goto exit; }; ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, HANDLER ": valid signature, subject=<%s>.", dn); if (ptr == NULL) { len = BIO_get_mem_data(out, &ptr); ptr = apr_pmemdup(r->pool, ptr, len); }; ret = apr_bucket_pool_create(ptr, len, r->pool, r->connection->bucket_alloc); exit: BIO_free(out); CMS_ContentInfo_free(ci); /* See https://groups.google.com/g/mailing.openssl.users/c/Vr32WbyIRRw/m/NBf8ofVl00gJ */ sk_X509_free(signers); if (dn) free(dn); return ret; } static apr_status_t _input_filter(ap_filter_t * f, apr_bucket_brigade * bbout, ap_input_mode_t eMode, apr_read_type_e eBlock, apr_off_t nBytes) { request_rec *r = f->r; verify_config_rec *conf = ap_get_module_config(r->per_dir_config, &cms_verify_module); apr_bucket_brigade *bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); cms_filter_state *state = f->ctx; conn_rec *c = r->connection; apr_status_t rv; if (conf == NULL || conf->trusted_certs == NULL || sk_X509_OBJECT_num(X509_STORE_get0_objects(conf->trusted_certs)) < 1 ) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, HANDLER ": No trusted cert(s) configured."); return HTTP_INTERNAL_SERVER_ERROR; } bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); if (state == NULL) { f->ctx = state = apr_palloc(r->pool, sizeof *state); state->pbb_tmp = apr_brigade_create(r->pool, c->bucket_alloc); apr_pool_cleanup_register(r->pool, state, state_cleanup, apr_pool_cleanup_null); state->in = BIO_new(BIO_s_mem()); state->lenin = 0; }; if (APR_BRIGADE_EMPTY(state->pbb_tmp)) { rv = ap_get_brigade(f->next, state->pbb_tmp, eMode, eBlock, nBytes); if (eMode == AP_MODE_EATCRLF || rv != APR_SUCCESS) return rv; } while (!APR_BRIGADE_EMPTY(state->pbb_tmp)) { apr_bucket *pbkt_in = APR_BRIGADE_FIRST(state->pbb_tmp); apr_bucket *pbkt_out; const char *data; apr_size_t len; if (APR_BUCKET_IS_EOS(pbkt_in)) { /* we're called with a second (empty) bucket brigade during the close * of the socket. So we ignore the second empty brigade. See this * comment https://svn.apache.org/viewvc/httpd/httpd/trunk/modules/ssl/ssl_engine_io.c?view=markup#l2205 and * https://lists.apache.org/thread.html/r45cf5de9ccbed93bb7ca6b341a73d3d69af409b34ad60b7cb65fe259%40%3Cdev.httpd.apache.org%3E. */ if (state->lenin) { state->lenin = 0; if (!(pbkt_out = _verify(r, state->in))) return HTTP_BAD_REQUEST; APR_BRIGADE_INSERT_TAIL(bbout, pbkt_out); }; APR_BRIGADE_INSERT_TAIL(bbout, apr_bucket_eos_create(r->connection->bucket_alloc)); APR_BUCKET_REMOVE(pbkt_in); break; } rv = apr_bucket_read(pbkt_in, &data, &len, eBlock); if (rv != APR_SUCCESS) return rv; if (len + state->lenin > conf->max_size) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, HANDLER ": PKCS#7 input exceeds max size."); return HTTP_INTERNAL_SERVER_ERROR; } /* We read this into memory rather than already pass it on to the brigade; as we can only * check the signature at the very end. */ while (len > 0) { int dlen = BIO_write(state->in, data, len); if (dlen == 0) break; if (dlen < 0) { apr_brigade_destroy(bb); ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, HANDLER ": could not write for verify: %s", ERR_reason_error_string(ERR_get_error())); return HTTP_INTERNAL_SERVER_ERROR; }; data += dlen; len -= dlen; state->lenin += dlen; }; apr_bucket_delete(pbkt_in); }; return APR_SUCCESS; } static int _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, _cleanup, apr_pool_cleanup_null); return APR_SUCCESS; } static void _register_hooks(apr_pool_t * p) { ap_hook_pre_config(_pre_config, NULL, NULL, APR_HOOK_MIDDLE); ap_register_input_filter(HANDLER, _input_filter, NULL, AP_FTYPE_CONNECTION /* AP_FTYPE_RESOURCE */); } static const command_rec _cmds[] = { AP_INIT_ITERATE("CMSVerifyCertificate", add_trusted_certs, NULL, RSRC_CONF | ACCESS_CONF, "Set one or more CMS verifying certificate(s). Typically a single CA or a list of whitelisted leafs."), AP_INIT_ITERATE("CMSVerifyUntrustedCertificate", add_untrusted_certs, NULL, RSRC_CONF | ACCESS_CONF, "Set one or more CMS verifying certificate that are untrusted (e.g. to complete a chain)."), AP_INIT_TAKE1("CMSVerifyMaxPKCS7Size", set_max_size, NULL, RSRC_CONF | ACCESS_CONF, "Set the max size of the PKCS7 data POSTed (defaults to " TOSTRING(MAX_PKCS7_SIZE) " bytes)."), {NULL} }; AP_DECLARE_MODULE(cms_verify) = { STANDARD20_MODULE_STUFF, _create_dir_config, /* dir config creater */ _merge_dir_config, /* dir merger --- default is to override */ NULL, /* server config */ NULL, /* merge server config */ _cmds, /* command apr_table_t */ _register_hooks /* register hooks */ };