/* 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 */
};