diff options
Diffstat (limited to 'modules/md')
42 files changed, 15439 insertions, 0 deletions
diff --git a/modules/md/Makefile.in b/modules/md/Makefile.in new file mode 100644 index 0000000..4395bc3 --- /dev/null +++ b/modules/md/Makefile.in @@ -0,0 +1,20 @@ +# 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. + +# +# standard stuff +# + +include $(top_srcdir)/build/special.mk diff --git a/modules/md/config2.m4 b/modules/md/config2.m4 new file mode 100644 index 0000000..a2c8303 --- /dev/null +++ b/modules/md/config2.m4 @@ -0,0 +1,300 @@ +dnl Licensed to the Apache Software Foundation (ASF) under one or more +dnl contributor license agreements. See the NOTICE file distributed with +dnl this work for additional information regarding copyright ownership. +dnl The ASF licenses this file to You under the Apache License, Version 2.0 +dnl (the "License"); you may not use this file except in compliance with +dnl the License. You may obtain a copy of the License at +dnl +dnl http://www.apache.org/licenses/LICENSE-2.0 +dnl +dnl Unless required by applicable law or agreed to in writing, software +dnl distributed under the License is distributed on an "AS IS" BASIS, +dnl WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +dnl See the License for the specific language governing permissions and +dnl limitations under the License. + +dnl +dnl APACHE_CHECK_CURL +dnl +dnl Configure for libcurl, giving preference to +dnl "--with-curl=<path>" if it was specified. +dnl +AC_DEFUN([APACHE_CHECK_CURL],[ + AC_CACHE_CHECK([for curl], [ac_cv_curl], [ + dnl initialise the variables we use + ac_cv_curl=no + ap_curl_found="" + ap_curl_base="" + ap_curl_libs="" + + dnl Determine the curl base directory, if any + AC_MSG_CHECKING([for user-provided curl base directory]) + AC_ARG_WITH(curl, APACHE_HELP_STRING(--with-curl=PATH, curl installation directory), [ + dnl If --with-curl specifies a directory, we use that directory + if test "x$withval" != "xyes" -a "x$withval" != "x"; then + dnl This ensures $withval is actually a directory and that it is absolute + ap_curl_base="`cd $withval ; pwd`" + fi + ]) + if test "x$ap_curl_base" = "x"; then + AC_MSG_RESULT(none) + else + AC_MSG_RESULT($ap_curl_base) + fi + + dnl Run header and version checks + saved_CPPFLAGS="$CPPFLAGS" + saved_LIBS="$LIBS" + saved_LDFLAGS="$LDFLAGS" + + dnl Before doing anything else, load in pkg-config variables + if test -n "$PKGCONFIG"; then + saved_PKG_CONFIG_PATH="$PKG_CONFIG_PATH" + AC_MSG_CHECKING([for pkg-config along $PKG_CONFIG_PATH]) + if test "x$ap_curl_base" != "x" ; then + if test -f "${ap_curl_base}/lib/pkgconfig/libcurl.pc"; then + dnl Ensure that the given path is used by pkg-config too, otherwise + dnl the system libcurl.pc might be picked up instead. + PKG_CONFIG_PATH="${ap_curl_base}/lib/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}" + export PKG_CONFIG_PATH + elif test -f "${ap_curl_base}/lib64/pkgconfig/libcurl.pc"; then + dnl Ensure that the given path is used by pkg-config too, otherwise + dnl the system libcurl.pc might be picked up instead. + PKG_CONFIG_PATH="${ap_curl_base}/lib64/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}" + export PKG_CONFIG_PATH + fi + fi + AC_ARG_ENABLE(curl-staticlib-deps,APACHE_HELP_STRING(--enable-curl-staticlib-deps,[link mod_md with dependencies of libcurl's static libraries (as indicated by "pkg-config --static"). Must be specified in addition to --enable-md.]), [ + if test "$enableval" = "yes"; then + PKGCONFIG_LIBOPTS="--static" + fi + ]) + ap_curl_libs="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-l --silence-errors libcurl`" + if test $? -eq 0; then + ap_curl_found="yes" + pkglookup="`$PKGCONFIG --cflags-only-I libcurl`" + APR_ADDTO(CPPFLAGS, [$pkglookup]) + APR_ADDTO(MOD_CFLAGS, [$pkglookup]) + pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-L libcurl`" + APR_ADDTO(LDFLAGS, [$pkglookup]) + APR_ADDTO(MOD_LDFLAGS, [$pkglookup]) + pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-other libcurl`" + APR_ADDTO(LDFLAGS, [$pkglookup]) + APR_ADDTO(MOD_LDFLAGS, [$pkglookup]) + fi + PKG_CONFIG_PATH="$saved_PKG_CONFIG_PATH" + fi + + dnl fall back to the user-supplied directory if not found via pkg-config + if test "x$ap_curl_base" != "x" -a "x$ap_curl_found" = "x"; then + APR_ADDTO(CPPFLAGS, [-I$ap_curl_base/include]) + APR_ADDTO(MOD_CFLAGS, [-I$ap_curl_base/include]) + APR_ADDTO(LDFLAGS, [-L$ap_curl_base/lib]) + APR_ADDTO(MOD_LDFLAGS, [-L$ap_curl_base/lib]) + if test "x$ap_platform_runtime_link_flag" != "x"; then + APR_ADDTO(LDFLAGS, [$ap_platform_runtime_link_flag$ap_curl_base/lib]) + APR_ADDTO(MOD_LDFLAGS, [$ap_platform_runtime_link_flag$ap_curl_base/lib]) + fi + fi + + AC_CHECK_HEADERS([curl/curl.h]) + + AC_MSG_CHECKING([for curl version >= 7.50]) + AC_TRY_COMPILE([#include <curl/curlver.h>],[ +#if !defined(LIBCURL_VERSION_MAJOR) +#error "Missing libcurl version" +#endif +#if LIBCURL_VERSION_MAJOR < 7 +#error "Unsupported libcurl version " LIBCURL_VERSION +#endif +#if LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR < 50 +#error "Unsupported libcurl version " LIBCURL_VERSION +#endif], + [AC_MSG_RESULT(OK) + ac_cv_curl=yes], + [AC_MSG_RESULT(FAILED)]) + + if test "x$ac_cv_curl" = "xyes"; then + ap_curl_libs="${ap_curl_libs:--lcurl} `$apr_config --libs`" + APR_ADDTO(MOD_LDFLAGS, [$ap_curl_libs]) + APR_ADDTO(LIBS, [$ap_curl_libs]) + fi + + dnl restore + CPPFLAGS="$saved_CPPFLAGS" + LIBS="$saved_LIBS" + LDFLAGS="$saved_LDFLAGS" + ]) + if test "x$ac_cv_curl" = "xyes"; then + AC_DEFINE(HAVE_CURL, 1, [Define if curl is available]) + fi +]) + + +dnl +dnl APACHE_CHECK_JANSSON +dnl +dnl Configure for libjansson, giving preference to +dnl "--with-jansson=<path>" if it was specified. +dnl +AC_DEFUN([APACHE_CHECK_JANSSON],[ + AC_CACHE_CHECK([for jansson], [ac_cv_jansson], [ + dnl initialise the variables we use + ac_cv_jansson=no + ap_jansson_found="" + ap_jansson_base="" + ap_jansson_libs="" + + dnl Determine the jansson base directory, if any + AC_MSG_CHECKING([for user-provided jansson base directory]) + AC_ARG_WITH(jansson, APACHE_HELP_STRING(--with-jansson=PATH, jansson installation directory), [ + dnl If --with-jansson specifies a directory, we use that directory + if test "x$withval" != "xyes" -a "x$withval" != "x"; then + dnl This ensures $withval is actually a directory and that it is absolute + ap_jansson_base="`cd $withval ; pwd`" + fi + ]) + if test "x$ap_jansson_base" = "x"; then + AC_MSG_RESULT(none) + else + AC_MSG_RESULT($ap_jansson_base) + fi + + dnl Run header and version checks + saved_CPPFLAGS="$CPPFLAGS" + saved_LIBS="$LIBS" + saved_LDFLAGS="$LDFLAGS" + + dnl Before doing anything else, load in pkg-config variables + if test -n "$PKGCONFIG"; then + saved_PKG_CONFIG_PATH="$PKG_CONFIG_PATH" + AC_MSG_CHECKING([for pkg-config along $PKG_CONFIG_PATH]) + if test "x$ap_jansson_base" != "x" ; then + if test -f "${ap_jansson_base}/lib/pkgconfig/libjansson.pc"; then + dnl Ensure that the given path is used by pkg-config too, otherwise + dnl the system libjansson.pc might be picked up instead. + PKG_CONFIG_PATH="${ap_jansson_base}/lib/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}" + export PKG_CONFIG_PATH + elif test -f "${ap_jansson_base}/lib64/pkgconfig/libjansson.pc"; then + dnl Ensure that the given path is used by pkg-config too, otherwise + dnl the system libjansson.pc might be picked up instead. + PKG_CONFIG_PATH="${ap_jansson_base}/lib64/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}" + export PKG_CONFIG_PATH + fi + fi + AC_ARG_ENABLE(jansson-staticlib-deps,APACHE_HELP_STRING(--enable-jansson-staticlib-deps,[link mod_md with dependencies of libjansson's static libraries (as indicated by "pkg-config --static"). Must be specified in addition to --enable-md.]), [ + if test "$enableval" = "yes"; then + PKGCONFIG_LIBOPTS="--static" + fi + ]) + ap_jansson_libs="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-l --silence-errors libjansson`" + if test $? -eq 0; then + ap_jansson_found="yes" + pkglookup="`$PKGCONFIG --cflags-only-I libjansson`" + APR_ADDTO(CPPFLAGS, [$pkglookup]) + APR_ADDTO(MOD_CFLAGS, [$pkglookup]) + pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-L libjansson`" + APR_ADDTO(LDFLAGS, [$pkglookup]) + APR_ADDTO(MOD_LDFLAGS, [$pkglookup]) + pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-other libjansson`" + APR_ADDTO(LDFLAGS, [$pkglookup]) + APR_ADDTO(MOD_LDFLAGS, [$pkglookup]) + fi + PKG_CONFIG_PATH="$saved_PKG_CONFIG_PATH" + fi + + dnl fall back to the user-supplied directory if not found via pkg-config + if test "x$ap_jansson_base" != "x" -a "x$ap_jansson_found" = "x"; then + APR_ADDTO(CPPFLAGS, [-I$ap_jansson_base/include]) + APR_ADDTO(MOD_CFLAGS, [-I$ap_jansson_base/include]) + APR_ADDTO(LDFLAGS, [-L$ap_jansson_base/lib]) + APR_ADDTO(MOD_LDFLAGS, [-L$ap_jansson_base/lib]) + if test "x$ap_platform_runtime_link_flag" != "x"; then + APR_ADDTO(LDFLAGS, [$ap_platform_runtime_link_flag$ap_jansson_base/lib]) + APR_ADDTO(MOD_LDFLAGS, [$ap_platform_runtime_link_flag$ap_jansson_base/lib]) + fi + fi + + # attempts to include jansson.h fail me. So lets make sure we can at least + # include its other header file + AC_TRY_COMPILE([#include <jansson_config.h>],[], + [AC_MSG_RESULT(OK) + ac_cv_jansson=yes], + [AC_MSG_RESULT(FAILED)]) + + if test "x$ac_cv_jansson" = "xyes"; then + ap_jansson_libs="${ap_jansson_libs:--ljansson} `$apr_config --libs`" + APR_ADDTO(MOD_LDFLAGS, [$ap_jansson_libs]) + APR_ADDTO(LIBS, [$ap_jansson_libs]) + fi + + dnl restore + CPPFLAGS="$saved_CPPFLAGS" + LIBS="$saved_LIBS" + LDFLAGS="$saved_LDFLAGS" + ]) + if test "x$ac_cv_jansson" = "xyes"; then + AC_DEFINE(HAVE_JANSSON, 1, [Define if jansson is available]) + fi +]) + + +dnl # start of module specific part +APACHE_MODPATH_INIT(md) + +dnl # list of module object files +md_objs="dnl +md_acme.lo dnl +md_acme_acct.lo dnl +md_acme_authz.lo dnl +md_acme_drive.lo dnl +md_core.lo dnl +md_curl.lo dnl +md_crypt.lo dnl +md_http.lo dnl +md_json.lo dnl +md_jws.lo dnl +md_log.lo dnl +md_reg.lo dnl +md_store.lo dnl +md_store_fs.lo dnl +md_util.lo dnl +mod_md.lo dnl +mod_md_config.lo dnl +mod_md_os.lo dnl +" + +# Ensure that other modules can pick up mod_md.h +APR_ADDTO(INCLUDES, [-I\$(top_srcdir)/$modpath_current]) + +dnl # hook module into the Autoconf mechanism (--enable-md) +APACHE_MODULE(md, [Managed Domain handling], $md_objs, , most, [ + APACHE_CHECK_OPENSSL + if test "x$ac_cv_openssl" = "xno" ; then + AC_MSG_WARN([libssl (or compatible) not found]) + enable_md=no + fi + + APACHE_CHECK_JANSSON + if test "x$ac_cv_jansson" != "xyes" ; then + AC_MSG_WARN([libjansson not found]) + enable_md=no + fi + + APACHE_CHECK_CURL + if test "x$ac_cv_curl" != "xyes" ; then + AC_MSG_WARN([libcurl not found]) + enable_md=no + fi + + AC_CHECK_FUNCS([arc4random_buf], + [APR_ADDTO(MOD_CPPFLAGS, ["-DMD_HAVE_ARC4RANDOM"])], []) + + if test "x$enable_md" = "xshared"; then + APR_ADDTO(MOD_MD_LDADD, [-export-symbols-regex md_module]) + fi +]) + +dnl # end of module specific part +APACHE_MODPATH_FINISH + diff --git a/modules/md/md.h b/modules/md/md.h new file mode 100644 index 0000000..60f8852 --- /dev/null +++ b/modules/md/md.h @@ -0,0 +1,290 @@ +/* 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. + */ + +#ifndef mod_md_md_h +#define mod_md_md_h + +#include "md_version.h" + +struct apr_array_header_t; +struct apr_hash_t; +struct md_json_t; +struct md_cert_t; +struct md_pkey_t; +struct md_store_t; +struct md_srv_conf_t; +struct md_pkey_spec_t; + +#define MD_TLSSNI01_DNS_SUFFIX ".acme.invalid" + +#define MD_PKEY_RSA_BITS_MIN 2048 +#define MD_PKEY_RSA_BITS_DEF 2048 + +/* Minimum age for the HSTS header (RFC 6797), considered appropriate by Mozilla Security */ +#define MD_HSTS_HEADER "Strict-Transport-Security" +#define MD_HSTS_MAX_AGE_DEFAULT 15768000 + +typedef enum { + MD_S_UNKNOWN, /* MD has not been analysed yet */ + MD_S_INCOMPLETE, /* MD is missing necessary information, cannot go live */ + MD_S_COMPLETE, /* MD has all necessary information, can go live */ + MD_S_EXPIRED, /* MD is complete, but credentials have expired */ + MD_S_ERROR, /* MD data is flawed, unable to be processed as is */ + MD_S_MISSING, /* MD is missing config information, cannot proceed */ +} md_state_t; + +typedef enum { + MD_REQUIRE_UNSET = -1, + MD_REQUIRE_OFF, + MD_REQUIRE_TEMPORARY, + MD_REQUIRE_PERMANENT, +} md_require_t; + +typedef enum { + MD_SV_TEXT, + MD_SV_JSON, + MD_SV_CERT, + MD_SV_PKEY, + MD_SV_CHAIN, +} md_store_vtype_t; + +typedef enum { + MD_SG_NONE, + MD_SG_ACCOUNTS, + MD_SG_CHALLENGES, + MD_SG_DOMAINS, + MD_SG_STAGING, + MD_SG_ARCHIVE, + MD_SG_TMP, + MD_SG_COUNT, +} md_store_group_t; + +typedef enum { + MD_DRIVE_DEFAULT = -1, /* default value */ + MD_DRIVE_MANUAL, /* manually triggered transmission of credentials */ + MD_DRIVE_AUTO, /* automatic process performed by httpd */ + MD_DRIVE_ALWAYS, /* always driven by httpd, even if not used in any vhost */ +} md_drive_mode_t; + +typedef struct md_t md_t; +struct md_t { + const char *name; /* unique name of this MD */ + struct apr_array_header_t *domains; /* all DNS names this MD includes */ + struct apr_array_header_t *contacts; /* list of contact uris, e.g. mailto:xxx */ + + int transitive; /* != 0 iff VirtualHost names/aliases are auto-added */ + md_require_t require_https; /* Iff https: is required for this MD */ + + int drive_mode; /* mode of obtaining credentials */ + struct md_pkey_spec_t *pkey_spec;/* specification for generating new private keys */ + int must_staple; /* certificates should set the OCSP Must Staple extension */ + apr_interval_time_t renew_norm; /* if > 0, normalized cert lifetime */ + apr_interval_time_t renew_window;/* time before expiration that starts renewal */ + + const char *ca_url; /* url of CA certificate service */ + const char *ca_proto; /* protocol used vs CA (e.g. ACME) */ + const char *ca_account; /* account used at CA */ + const char *ca_agreement; /* accepted agreement uri between CA and user */ + struct apr_array_header_t *ca_challenges; /* challenge types configured for this MD */ + + md_state_t state; /* state of this MD */ + apr_time_t valid_from; /* When the credentials start to be valid. 0 if unknown */ + apr_time_t expires; /* When the credentials expire. 0 if unknown */ + const char *cert_url; /* url where cert has been created, remember during drive */ + + const struct md_srv_conf_t *sc; /* server config where it was defined or NULL */ + const char *defn_name; /* config file this MD was defined */ + unsigned defn_line_number; /* line number of definition */ +}; + +#define MD_KEY_ACCOUNT "account" +#define MD_KEY_AGREEMENT "agreement" +#define MD_KEY_BITS "bits" +#define MD_KEY_CA "ca" +#define MD_KEY_CA_URL "ca-url" +#define MD_KEY_CERT "cert" +#define MD_KEY_CHALLENGES "challenges" +#define MD_KEY_CONTACT "contact" +#define MD_KEY_CONTACTS "contacts" +#define MD_KEY_CSR "csr" +#define MD_KEY_DETAIL "detail" +#define MD_KEY_DISABLED "disabled" +#define MD_KEY_DIR "dir" +#define MD_KEY_DOMAIN "domain" +#define MD_KEY_DOMAINS "domains" +#define MD_KEY_DRIVE_MODE "drive-mode" +#define MD_KEY_ERRORS "errors" +#define MD_KEY_EXPIRES "expires" +#define MD_KEY_HTTP "http" +#define MD_KEY_HTTPS "https" +#define MD_KEY_ID "id" +#define MD_KEY_IDENTIFIER "identifier" +#define MD_KEY_KEY "key" +#define MD_KEY_KEYAUTHZ "keyAuthorization" +#define MD_KEY_LOCATION "location" +#define MD_KEY_MUST_STAPLE "must-staple" +#define MD_KEY_NAME "name" +#define MD_KEY_PERMANENT "permanent" +#define MD_KEY_PKEY "privkey" +#define MD_KEY_PROCESSED "processed" +#define MD_KEY_PROTO "proto" +#define MD_KEY_REGISTRATION "registration" +#define MD_KEY_RENEW "renew" +#define MD_KEY_RENEW_WINDOW "renew-window" +#define MD_KEY_REQUIRE_HTTPS "require-https" +#define MD_KEY_RESOURCE "resource" +#define MD_KEY_STATE "state" +#define MD_KEY_STATUS "status" +#define MD_KEY_STORE "store" +#define MD_KEY_TEMPORARY "temporary" +#define MD_KEY_TOKEN "token" +#define MD_KEY_TRANSITIVE "transitive" +#define MD_KEY_TYPE "type" +#define MD_KEY_URL "url" +#define MD_KEY_URI "uri" +#define MD_KEY_VALID_FROM "validFrom" +#define MD_KEY_VALUE "value" +#define MD_KEY_VERSION "version" + +#define MD_FN_MD "md.json" +#define MD_FN_JOB "job.json" +#define MD_FN_PRIVKEY "privkey.pem" +#define MD_FN_PUBCERT "pubcert.pem" +#define MD_FN_CERT "cert.pem" +#define MD_FN_CHAIN "chain.pem" +#define MD_FN_HTTPD_JSON "httpd.json" + +#define MD_FN_FALLBACK_PKEY "fallback-privkey.pem" +#define MD_FN_FALLBACK_CERT "fallback-cert.pem" + +/* Check if a string member of a new MD (n) has + * a value and if it differs from the old MD o + */ +#define MD_VAL_UPDATE(n,o,s) ((n)->s != (o)->s) +#define MD_SVAL_UPDATE(n,o,s) ((n)->s && (!(o)->s || strcmp((n)->s, (o)->s))) + +/** + * Determine if the Managed Domain contains a specific domain name. + */ +int md_contains(const md_t *md, const char *domain, int case_sensitive); + +/** + * Determine if the names of the two managed domains overlap. + */ +int md_domains_overlap(const md_t *md1, const md_t *md2); + +/** + * Determine if the domain names are equal. + */ +int md_equal_domains(const md_t *md1, const md_t *md2, int case_sensitive); + +/** + * Determine if the domains in md1 contain all domains of md2. + */ +int md_contains_domains(const md_t *md1, const md_t *md2); + +/** + * Get one common domain name of the two managed domains or NULL. + */ +const char *md_common_name(const md_t *md1, const md_t *md2); + +/** + * Get the number of common domains. + */ +apr_size_t md_common_name_count(const md_t *md1, const md_t *md2); + +/** + * Look up a managed domain by its name. + */ +md_t *md_get_by_name(struct apr_array_header_t *mds, const char *name); + +/** + * Look up a managed domain by a DNS name it contains. + */ +md_t *md_get_by_domain(struct apr_array_header_t *mds, const char *domain); + +/** + * Find a managed domain, different from the given one, that has overlaps + * in the domain list. + */ +md_t *md_get_by_dns_overlap(struct apr_array_header_t *mds, const md_t *md); + +/** + * Find the managed domain in the list that, for the given md, + * has the same name, or the most number of overlaps in domains + */ +md_t *md_find_closest_match(apr_array_header_t *mds, const md_t *md); + +/** + * Create and empty md record, structures initialized. + */ +md_t *md_create_empty(apr_pool_t *p); + +/** + * Create a managed domain, given a list of domain names. + */ +md_t *md_create(apr_pool_t *p, struct apr_array_header_t *domains); + +/** + * Deep copy an md record into another pool. + */ +md_t *md_clone(apr_pool_t *p, const md_t *src); + +/** + * Shallow copy an md record into another pool. + */ +md_t *md_copy(apr_pool_t *p, const md_t *src); + +/** + * Create a merged md with the settings of add overlaying the ones from base. + */ +md_t *md_merge(apr_pool_t *p, const md_t *add, const md_t *base); + +/** + * Convert the managed domain into a JSON representation and vice versa. + * + * This reads and writes the following information: name, domains, ca_url, ca_proto and state. + */ +struct md_json_t *md_to_json (const md_t *md, apr_pool_t *p); +md_t *md_from_json(struct md_json_t *json, apr_pool_t *p); + +/** + * Determine if MD should renew its cert (if it has one) + */ +int md_should_renew(const md_t *md); + +/**************************************************************************************************/ +/* domain credentials */ + +typedef struct md_creds_t md_creds_t; +struct md_creds_t { + struct md_pkey_t *privkey; + struct apr_array_header_t *pubcert; /* complete md_cert* chain */ + struct md_cert_t *cert; + int expired; +}; + +/* TODO: not sure this is a good idea, testing some readability and debuggabiltiy of + * cascaded apr_status_t checks. */ +#define MD_CHK_VARS const char *md_chk_ +#define MD_LAST_CHK md_chk_ +#define MD_CHK_STEP(c, status, s) (md_chk_ = s, (void)md_chk_, status == (rv = (c))) +#define MD_CHK(c, status) MD_CHK_STEP(c, status, #c) +#define MD_IS_ERR(c, err) (md_chk_ = #c, APR_STATUS_IS_##err((rv = (c)))) +#define MD_CHK_SUCCESS(c) MD_CHK(c, APR_SUCCESS) +#define MD_OK(c) MD_CHK_SUCCESS(c) + +#endif /* mod_md_md_h */ diff --git a/modules/md/md_acme.c b/modules/md/md_acme.c new file mode 100644 index 0000000..3fbd365 --- /dev/null +++ b/modules/md/md_acme.c @@ -0,0 +1,531 @@ +/* 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. + */ + +#include <assert.h> +#include <stdlib.h> + +#include <apr_lib.h> +#include <apr_strings.h> +#include <apr_buckets.h> +#include <apr_hash.h> +#include <apr_uri.h> + +#include "md.h" +#include "md_crypt.h" +#include "md_json.h" +#include "md_jws.h" +#include "md_http.h" +#include "md_log.h" +#include "md_store.h" +#include "md_util.h" +#include "md_version.h" + +#include "md_acme.h" +#include "md_acme_acct.h" + + +static const char *base_product; + +typedef struct acme_problem_status_t acme_problem_status_t; + +struct acme_problem_status_t { + const char *type; + apr_status_t rv; +}; + +static acme_problem_status_t Problems[] = { + { "acme:error:badCSR", APR_EINVAL }, + { "acme:error:badNonce", APR_EAGAIN }, + { "acme:error:badSignatureAlgorithm", APR_EINVAL }, + { "acme:error:invalidContact", APR_BADARG }, + { "acme:error:unsupportedContact", APR_EGENERAL }, + { "acme:error:malformed", APR_EINVAL }, + { "acme:error:rateLimited", APR_BADARG }, + { "acme:error:rejectedIdentifier", APR_BADARG }, + { "acme:error:serverInternal", APR_EGENERAL }, + { "acme:error:unauthorized", APR_EACCES }, + { "acme:error:unsupportedIdentifier", APR_BADARG }, + { "acme:error:userActionRequired", APR_EAGAIN }, + { "acme:error:badRevocationReason", APR_EINVAL }, + { "acme:error:caa", APR_EGENERAL }, + { "acme:error:dns", APR_EGENERAL }, + { "acme:error:connection", APR_EGENERAL }, + { "acme:error:tls", APR_EGENERAL }, + { "acme:error:incorrectResponse", APR_EGENERAL }, +}; + +static apr_status_t problem_status_get(const char *type) { + size_t i; + + if (strstr(type, "urn:ietf:params:") == type) { + type += strlen("urn:ietf:params:"); + } + else if (strstr(type, "urn:") == type) { + type += strlen("urn:"); + } + + for(i = 0; i < (sizeof(Problems)/sizeof(Problems[0])); ++i) { + if (!apr_strnatcasecmp(type, Problems[i].type)) { + return Problems[i].rv; + } + } + return APR_EGENERAL; +} + +apr_status_t md_acme_init(apr_pool_t *p, const char *base) +{ + base_product = base; + return md_crypt_init(p); +} + +apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url, + const char *proxy_url) +{ + md_acme_t *acme; + const char *err = NULL; + apr_status_t rv; + apr_uri_t uri_parsed; + size_t len; + + if (!url) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, "create ACME without url"); + return APR_EINVAL; + } + + if (APR_SUCCESS != (rv = md_util_abs_uri_check(p, url, &err))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "invalid ACME uri (%s): %s", err, url); + return rv; + } + + acme = apr_pcalloc(p, sizeof(*acme)); + acme->url = url; + acme->p = p; + acme->user_agent = apr_psprintf(p, "%s mod_md/%s", + base_product, MOD_MD_VERSION); + acme->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL; + acme->max_retries = 3; + + if (APR_SUCCESS != (rv = apr_uri_parse(p, url, &uri_parsed))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "parsing ACME uri: %s", url); + return APR_EINVAL; + } + + len = strlen(uri_parsed.hostname); + acme->sname = (len <= 16)? uri_parsed.hostname : apr_pstrdup(p, uri_parsed.hostname + len - 16); + + *pacme = (APR_SUCCESS == rv)? acme : NULL; + return rv; +} + +apr_status_t md_acme_setup(md_acme_t *acme) +{ + apr_status_t rv; + md_json_t *json; + + assert(acme->url); + if (!acme->http && APR_SUCCESS != (rv = md_http_create(&acme->http, acme->p, + acme->user_agent, acme->proxy_url))) { + return rv; + } + md_http_set_response_limit(acme->http, 1024*1024); + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "get directory from %s", acme->url); + + rv = md_acme_get_json(&json, acme, acme->url, acme->p); + if (APR_SUCCESS == rv) { + acme->new_authz = md_json_gets(json, "new-authz", NULL); + acme->new_cert = md_json_gets(json, "new-cert", NULL); + acme->new_reg = md_json_gets(json, "new-reg", NULL); + acme->revoke_cert = md_json_gets(json, "revoke-cert", NULL); + if (acme->new_authz && acme->new_cert && acme->new_reg && acme->revoke_cert) { + return APR_SUCCESS; + } + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, acme->p, + "Unable to understand ACME server response. Wrong ACME protocol version?"); + rv = APR_EINVAL; + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, acme->p, "unsuccessful in contacting ACME " + "server at %s. If this problem persists, please check your network " + "connectivity from your Apache server to the ACME server. Also, older " + "servers might have trouble verifying the certificates of the ACME " + "server. You can check if you are able to contact it manually via the " + "curl command. Sometimes, the ACME server might be down for maintenance, " + "so failing to contact it is not an immediate problem. mod_md will " + "continue retrying this.", acme->url); + } + return rv; +} + +/**************************************************************************************************/ +/* acme requests */ + +static void req_update_nonce(md_acme_t *acme, apr_table_t *hdrs) +{ + if (hdrs) { + const char *nonce = apr_table_get(hdrs, "Replay-Nonce"); + if (nonce) { + acme->nonce = apr_pstrdup(acme->p, nonce); + } + } +} + +static apr_status_t http_update_nonce(const md_http_response_t *res) +{ + if (res->headers) { + const char *nonce = apr_table_get(res->headers, "Replay-Nonce"); + if (nonce) { + md_acme_t *acme = res->req->baton; + acme->nonce = apr_pstrdup(acme->p, nonce); + } + } + return res->rv; +} + +static apr_status_t md_acme_new_nonce(md_acme_t *acme) +{ + apr_status_t rv; + long id; + + rv = md_http_HEAD(acme->http, acme->new_reg, NULL, http_update_nonce, acme, &id); + md_http_await(acme->http, id); + return rv; +} + +static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, const char *url) +{ + apr_pool_t *pool; + md_acme_req_t *req; + apr_status_t rv; + + rv = apr_pool_create(&pool, acme->p); + if (rv != APR_SUCCESS) { + return NULL; + } + + req = apr_pcalloc(pool, sizeof(*req)); + if (!req) { + apr_pool_destroy(pool); + return NULL; + } + + req->acme = acme; + req->p = pool; + req->method = method; + req->url = url; + req->prot_hdrs = apr_table_make(pool, 5); + if (!req->prot_hdrs) { + apr_pool_destroy(pool); + return NULL; + } + req->max_retries = acme->max_retries; + + return req; +} + +apr_status_t md_acme_req_body_init(md_acme_req_t *req, md_json_t *jpayload) +{ + const char *payload; + size_t payload_len; + + if (!req->acme->acct) { + return APR_EINVAL; + } + + payload = md_json_writep(jpayload, req->p, MD_JSON_FMT_COMPACT); + if (!payload) { + return APR_EINVAL; + } + + payload_len = strlen(payload); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->p, + "acct payload(len=%" APR_SIZE_T_FMT "): %s", payload_len, payload); + return md_jws_sign(&req->req_json, req->p, payload, payload_len, + req->prot_hdrs, req->acme->acct_key, NULL); +} + + +static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t *res) +{ + const char *ctype; + md_json_t *problem; + + ctype = apr_table_get(req->resp_hdrs, "content-type"); + if (ctype && !strcmp(ctype, "application/problem+json")) { + /* RFC 7807 */ + md_json_read_http(&problem, req->p, res); + if (problem) { + const char *ptype, *pdetail; + + req->resp_json = problem; + ptype = md_json_gets(problem, MD_KEY_TYPE, NULL); + pdetail = md_json_gets(problem, MD_KEY_DETAIL, NULL); + req->rv = problem_status_get(ptype); + + if (APR_STATUS_IS_EAGAIN(req->rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, req->rv, req->p, + "acme reports %s: %s", ptype, pdetail); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, req->rv, req->p, + "acme problem %s: %s", ptype, pdetail); + } + return req->rv; + } + } + + if (APR_SUCCESS == res->rv) { + switch (res->status) { + case 400: + return APR_EINVAL; + case 403: + return APR_EACCES; + case 404: + return APR_ENOENT; + default: + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, req->p, + "acme problem unknown: http status %d", res->status); + return APR_EGENERAL; + } + } + return res->rv; +} + +/**************************************************************************************************/ +/* ACME requests with nonce handling */ + +static apr_status_t md_acme_req_done(md_acme_req_t *req) +{ + apr_status_t rv = req->rv; + if (req->p) { + apr_pool_destroy(req->p); + } + return rv; +} + +static apr_status_t on_response(const md_http_response_t *res) +{ + md_acme_req_t *req = res->req->baton; + apr_status_t rv = res->rv; + + if (APR_SUCCESS != rv) { + goto out; + } + + req->resp_hdrs = apr_table_clone(req->p, res->headers); + req_update_nonce(req->acme, res->headers); + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, req->p, "response: %d", res->status); + if (res->status >= 200 && res->status < 300) { + int processed = 0; + + if (req->on_json) { + processed = 1; + rv = md_json_read_http(&req->resp_json, req->p, res); + if (APR_SUCCESS == rv) { + if (md_log_is_level(req->p, MD_LOG_TRACE2)) { + const char *s; + s = md_json_writep(req->resp_json, req->p, MD_JSON_FMT_INDENT); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, req->p, + "response: %s", + s ? s : "<failed to serialize!>"); + } + rv = req->on_json(req->acme, req->p, req->resp_hdrs, req->resp_json, req->baton); + } + else if (APR_STATUS_IS_ENOENT(rv)) { + /* not JSON content, fall through */ + processed = 0; + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, req->p, "parsing JSON body"); + } + } + + if (!processed && req->on_res) { + processed = 1; + rv = req->on_res(req->acme, res, req->baton); + } + + if (!processed) { + rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, req->p, + "response: %d, content-type=%s", res->status, + apr_table_get(res->headers, "Content-Type")); + } + } + else if (APR_EAGAIN == (rv = inspect_problem(req, res))) { + /* leave req alive */ + return rv; + } + +out: + md_acme_req_done(req); + return rv; +} + +static apr_status_t md_acme_req_send(md_acme_req_t *req) +{ + apr_status_t rv; + md_acme_t *acme = req->acme; + const char *body = NULL; + + assert(acme->url); + + if (strcmp("GET", req->method) && strcmp("HEAD", req->method)) { + if (!acme->new_authz) { + if (APR_SUCCESS != (rv = md_acme_setup(acme))) { + return rv; + } + } + if (!acme->nonce) { + if (APR_SUCCESS != (rv = md_acme_new_nonce(acme))) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, req->p, + "error retrieving new nonce from ACME server"); + return rv; + } + } + + apr_table_set(req->prot_hdrs, "nonce", acme->nonce); + acme->nonce = NULL; + } + + rv = req->on_init? req->on_init(req, req->baton) : APR_SUCCESS; + + if ((rv == APR_SUCCESS) && req->req_json) { + body = md_json_writep(req->req_json, req->p, MD_JSON_FMT_INDENT); + if (!body) { + rv = APR_EINVAL; + } + } + + if (rv == APR_SUCCESS) { + long id = 0; + + if (body && md_log_is_level(req->p, MD_LOG_TRACE2)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, req->p, + "req: POST %s, body:\n%s", req->url, body); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, req->p, + "req: POST %s", req->url); + } + if (!strcmp("GET", req->method)) { + rv = md_http_GET(req->acme->http, req->url, NULL, on_response, req, &id); + } + else if (!strcmp("POST", req->method)) { + rv = md_http_POSTd(req->acme->http, req->url, NULL, "application/json", + body, body? strlen(body) : 0, on_response, req, &id); + } + else if (!strcmp("HEAD", req->method)) { + rv = md_http_HEAD(req->acme->http, req->url, NULL, on_response, req, &id); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, req->p, + "HTTP method %s against: %s", req->method, req->url); + rv = APR_ENOTIMPL; + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, req->p, "req sent"); + md_http_await(acme->http, id); + + if (APR_EAGAIN == rv && req->max_retries > 0) { + --req->max_retries; + return md_acme_req_send(req); + } + req = NULL; + } + + if (req) { + md_acme_req_done(req); + } + return rv; +} + +apr_status_t md_acme_POST(md_acme_t *acme, const char *url, + md_acme_req_init_cb *on_init, + md_acme_req_json_cb *on_json, + md_acme_req_res_cb *on_res, + void *baton) +{ + md_acme_req_t *req; + + assert(url); + assert(on_json || on_res); + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, acme->p, "add acme POST: %s", url); + req = md_acme_req_create(acme, "POST", url); + req->on_init = on_init; + req->on_json = on_json; + req->on_res = on_res; + req->baton = baton; + + return md_acme_req_send(req); +} + +apr_status_t md_acme_GET(md_acme_t *acme, const char *url, + md_acme_req_init_cb *on_init, + md_acme_req_json_cb *on_json, + md_acme_req_res_cb *on_res, + void *baton) +{ + md_acme_req_t *req; + + assert(url); + assert(on_json || on_res); + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, acme->p, "add acme GET: %s", url); + req = md_acme_req_create(acme, "GET", url); + req->on_init = on_init; + req->on_json = on_json; + req->on_res = on_res; + req->baton = baton; + + return md_acme_req_send(req); +} + +/**************************************************************************************************/ +/* GET JSON */ + +typedef struct { + apr_pool_t *pool; + md_json_t *json; +} json_ctx; + +static apr_status_t on_got_json(md_acme_t *acme, apr_pool_t *p, const apr_table_t *headers, + md_json_t *jbody, void *baton) +{ + json_ctx *ctx = baton; + + (void)acme; + (void)p; + (void)headers; + ctx->json = md_json_clone(ctx->pool, jbody); + return APR_SUCCESS; +} + +apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme, + const char *url, apr_pool_t *p) +{ + apr_status_t rv; + json_ctx ctx; + + ctx.pool = p; + ctx.json = NULL; + + rv = md_acme_GET(acme, url, NULL, on_got_json, NULL, &ctx); + *pjson = (APR_SUCCESS == rv)? ctx.json : NULL; + return rv; +} + diff --git a/modules/md/md_acme.h b/modules/md/md_acme.h new file mode 100644 index 0000000..2dcbee6 --- /dev/null +++ b/modules/md/md_acme.h @@ -0,0 +1,267 @@ +/* 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. + */ + +#ifndef mod_md_md_acme_h +#define mod_md_md_acme_h + +struct apr_array_header_t; +struct apr_bucket_brigade; +struct md_http_response_t; +struct apr_hash_t; +struct md_http_t; +struct md_json_t; +struct md_pkey_t; +struct md_t; +struct md_acme_acct_t; +struct md_proto_t; +struct md_store_t; + +#define MD_PROTO_ACME "ACME" + +#define MD_AUTHZ_CHA_HTTP_01 "http-01" +#define MD_AUTHZ_CHA_SNI_01 "tls-sni-01" + +typedef enum { + MD_ACME_S_UNKNOWN, /* MD has not been analysed yet */ + MD_ACME_S_REGISTERED, /* MD is registered at CA, but not more */ + MD_ACME_S_TOS_ACCEPTED, /* Terms of Service were accepted by account holder */ + MD_ACME_S_CHALLENGED, /* MD challenge information for all domains is known */ + MD_ACME_S_VALIDATED, /* MD domains have been validated */ + MD_ACME_S_CERTIFIED, /* MD has valid certificate */ + MD_ACME_S_DENIED, /* MD domains (at least one) have been denied by CA */ +} md_acme_state_t; + +typedef struct md_acme_t md_acme_t; + +struct md_acme_t { + const char *url; /* directory url of the ACME service */ + const char *sname; /* short name for the service, not necessarily unique */ + apr_pool_t *p; + const char *user_agent; + const char *proxy_url; + struct md_acme_acct_t *acct; + struct md_pkey_t *acct_key; + + const char *new_authz; + const char *new_cert; + const char *new_reg; + const char *revoke_cert; + + struct md_http_t *http; + + const char *nonce; + int max_retries; +}; + +/** + * Global init, call once at start up. + */ +apr_status_t md_acme_init(apr_pool_t *pool, const char *base_version); + +/** + * Create a new ACME server instance. If path is not NULL, will use that directory + * for persisting information. Will load any information persisted in earlier session. + * url needs only be specified for instances where this has never been persisted before. + * + * @param pacme will hold the ACME server instance on success + * @param p pool to used + * @param url url of the server, optional if known at path + * @param proxy_url optional url of a HTTP(S) proxy to use + */ +apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url, + const char *proxy_url); + +/** + * Contact the ACME server and retrieve its directory information. + * + * @param acme the ACME server to contact + */ +apr_status_t md_acme_setup(md_acme_t *acme); + +/**************************************************************************************************/ +/* account handling */ + +#define MD_ACME_ACCT_STAGED "staged" + +apr_status_t md_acme_acct_load(struct md_acme_acct_t **pacct, struct md_pkey_t **ppkey, + struct md_store_t *store, md_store_group_t group, + const char *name, apr_pool_t *p); + +/** + * Specify the account to use by name in local store. On success, the account + * the "current" one used by the acme instance. + */ +apr_status_t md_acme_use_acct(md_acme_t *acme, struct md_store_t *store, + apr_pool_t *p, const char *acct_id); + +apr_status_t md_acme_use_acct_staged(md_acme_t *acme, struct md_store_t *store, + md_t *md, apr_pool_t *p); + +/** + * Get the local name of the account currently used by the acme instance. + * Will be NULL if no account has been setup successfully. + */ +const char *md_acme_get_acct_id(md_acme_t *acme); + +/** + * Agree to the given Terms-of-Service url for the current account. + */ +apr_status_t md_acme_agree(md_acme_t *acme, apr_pool_t *p, const char *tos); + +/** + * Confirm with the server that the current account agrees to the Terms-of-Service + * given in the agreement url. + * If the known agreement is equal to this, nothing is done. + * If it differs, the account is re-validated in the hope that the server + * announces the Tos URL it wants. If this is equal to the agreement specified, + * the server is notified of this. If the server requires a ToS that the account + * thinks it has already given, it is resend. + * + * If an agreement is required, different from the current one, APR_INCOMPLETE is + * returned and the agreement url is returned in the parameter. + */ +apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p, + const char *agreement, const char **prequired); + +/** + * Get the ToS agreement for current account. + */ +const char *md_acme_get_agreement(md_acme_t *acme); + + +/** + * Find an existing account in the local store. On APR_SUCCESS, the acme + * instance will have a current, validated account to use. + */ +apr_status_t md_acme_find_acct(md_acme_t *acme, struct md_store_t *store, apr_pool_t *p); + +/** + * Create a new account at the ACME server. The + * new account is the one used by the acme instance afterwards, on success. + */ +apr_status_t md_acme_create_acct(md_acme_t *acme, apr_pool_t *p, apr_array_header_t *contacts, + const char *agreement); + +apr_status_t md_acme_acct_save(struct md_store_t *store, apr_pool_t *p, md_acme_t *acme, + struct md_acme_acct_t *acct, struct md_pkey_t *acct_key); + +apr_status_t md_acme_save(md_acme_t *acme, struct md_store_t *store, apr_pool_t *p); + +apr_status_t md_acme_acct_save_staged(md_acme_t *acme, struct md_store_t *store, + md_t *md, apr_pool_t *p); + +/** + * Delete the current account at the ACME server and remove it from store. + */ +apr_status_t md_acme_delete_acct(md_acme_t *acme, struct md_store_t *store, apr_pool_t *p); + +/** + * Delete the account from the local store without contacting the ACME server. + */ +apr_status_t md_acme_unstore_acct(struct md_store_t *store, apr_pool_t *p, const char *acct_id); + +/**************************************************************************************************/ +/* request handling */ + +/** + * Request callback on a successful HTTP response (status 2xx). + */ +typedef apr_status_t md_acme_req_res_cb(md_acme_t *acme, + const struct md_http_response_t *res, void *baton); + +/** + * A request against an ACME server + */ +typedef struct md_acme_req_t md_acme_req_t; + +/** + * Request callback to initialize before sending. May be invoked more than once in + * case of retries. + */ +typedef apr_status_t md_acme_req_init_cb(md_acme_req_t *req, void *baton); + +/** + * Request callback on a successful response (HTTP response code 2xx) and content + * type matching application/.*json. + */ +typedef apr_status_t md_acme_req_json_cb(md_acme_t *acme, apr_pool_t *p, + const apr_table_t *headers, + struct md_json_t *jbody, void *baton); + +struct md_acme_req_t { + md_acme_t *acme; /* the ACME server to talk to */ + apr_pool_t *p; /* pool for the request duration */ + + const char *url; /* url to POST the request to */ + const char *method; /* HTTP method to use */ + apr_table_t *prot_hdrs; /* JWS headers needing protection (nonce) */ + struct md_json_t *req_json; /* JSON to be POSTed in request body */ + + apr_table_t *resp_hdrs; /* HTTP response headers */ + struct md_json_t *resp_json; /* JSON response body received */ + + apr_status_t rv; /* status of request */ + + md_acme_req_init_cb *on_init; /* callback to initialize the request before submit */ + md_acme_req_json_cb *on_json; /* callback on successful JSON response */ + md_acme_req_res_cb *on_res; /* callback on generic HTTP response */ + int max_retries; /* how often this might be retried */ + void *baton; /* userdata for callbacks */ +}; + +apr_status_t md_acme_GET(md_acme_t *acme, const char *url, + md_acme_req_init_cb *on_init, + md_acme_req_json_cb *on_json, + md_acme_req_res_cb *on_res, + void *baton); +/** + * Perform a POST against the ACME url. If a on_json callback is given and + * the HTTP response is JSON, only this callback is invoked. Otherwise, on HTTP status + * 2xx, the on_res callback is invoked. If no on_res is given, it is considered a + * response error, since only JSON was expected. + * At least one callback needs to be non-NULL. + * + * @param acme the ACME server to talk to + * @param url the url to send the request to + * @param on_init callback to initialize the request data + * @param on_json callback on successful JSON response + * @param on_res callback on successful HTTP response + * @param baton userdata for callbacks + */ +apr_status_t md_acme_POST(md_acme_t *acme, const char *url, + md_acme_req_init_cb *on_init, + md_acme_req_json_cb *on_json, + md_acme_req_res_cb *on_res, + void *baton); + +apr_status_t md_acme_GET(md_acme_t *acme, const char *url, + md_acme_req_init_cb *on_init, + md_acme_req_json_cb *on_json, + md_acme_req_res_cb *on_res, + void *baton); + +/** + * Retrieve a JSON resource from the ACME server + */ +apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme, + const char *url, apr_pool_t *p); + + +apr_status_t md_acme_req_body_init(md_acme_req_t *req, struct md_json_t *jpayload); + +apr_status_t md_acme_protos_add(struct apr_hash_t *protos, apr_pool_t *p); + +#endif /* md_acme_h */ diff --git a/modules/md/md_acme_acct.c b/modules/md/md_acme_acct.c new file mode 100644 index 0000000..c4a2b5f --- /dev/null +++ b/modules/md/md_acme_acct.c @@ -0,0 +1,670 @@ +/* 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. + */ + +#include <assert.h> +#include <stdio.h> + +#include <apr_lib.h> +#include <apr_file_info.h> +#include <apr_file_io.h> +#include <apr_fnmatch.h> +#include <apr_hash.h> +#include <apr_strings.h> +#include <apr_tables.h> + +#include "md.h" +#include "md_crypt.h" +#include "md_json.h" +#include "md_jws.h" +#include "md_log.h" +#include "md_store.h" +#include "md_util.h" +#include "md_version.h" + +#include "md_acme.h" +#include "md_acme_acct.h" + +static apr_status_t acct_make(md_acme_acct_t **pacct, apr_pool_t *p, + const char *ca_url, const char *id, apr_array_header_t *contacts) +{ + md_acme_acct_t *acct; + + acct = apr_pcalloc(p, sizeof(*acct)); + + acct->id = id? apr_pstrdup(p, id) : NULL; + acct->ca_url = ca_url; + + if (!contacts || apr_is_empty_array(contacts)) { + acct->contacts = apr_array_make(p, 5, sizeof(const char *)); + } + else { + acct->contacts = apr_array_copy(p, contacts); + } + + *pacct = acct; + return APR_SUCCESS; +} + + +static const char *mk_acct_id(apr_pool_t *p, md_acme_t *acme, int i) +{ + return apr_psprintf(p, "ACME-%s-%04d", acme->sname, i); +} + +static const char *mk_acct_pattern(apr_pool_t *p, md_acme_t *acme) +{ + return apr_psprintf(p, "ACME-%s-*", acme->sname); +} + +/**************************************************************************************************/ +/* json load/save */ + +static md_json_t *acct_to_json(md_acme_acct_t *acct, apr_pool_t *p) +{ + md_json_t *jacct; + + assert(acct); + jacct = md_json_create(p); + md_json_sets(acct->id, jacct, MD_KEY_ID, NULL); + md_json_setb(acct->disabled, jacct, MD_KEY_DISABLED, NULL); + md_json_sets(acct->url, jacct, MD_KEY_URL, NULL); + md_json_sets(acct->ca_url, jacct, MD_KEY_CA_URL, NULL); + md_json_setj(acct->registration, jacct, MD_KEY_REGISTRATION, NULL); + if (acct->agreement) { + md_json_sets(acct->agreement, jacct, MD_KEY_AGREEMENT, NULL); + } + + return jacct; +} + +static apr_status_t acct_from_json(md_acme_acct_t **pacct, md_json_t *json, apr_pool_t *p) +{ + apr_status_t rv = APR_EINVAL; + md_acme_acct_t *acct; + int disabled; + const char *ca_url, *url, *id; + apr_array_header_t *contacts; + + id = md_json_gets(json, MD_KEY_ID, NULL); + disabled = md_json_getb(json, MD_KEY_DISABLED, NULL); + ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL); + if (!ca_url) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no CA url: %s", id); + goto out; + } + + url = md_json_gets(json, MD_KEY_URL, NULL); + if (!url) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no url: %s", id); + goto out; + } + + contacts = apr_array_make(p, 5, sizeof(const char *)); + md_json_getsa(contacts, json, MD_KEY_REGISTRATION, MD_KEY_CONTACT, NULL); + rv = acct_make(&acct, p, ca_url, id, contacts); + if (APR_SUCCESS == rv) { + acct->disabled = disabled; + acct->url = url; + acct->agreement = md_json_gets(json, "terms-of-service", NULL); + } + +out: + *pacct = (APR_SUCCESS == rv)? acct : NULL; + return rv; +} + +apr_status_t md_acme_acct_save_staged(md_acme_t *acme, md_store_t *store, md_t *md, apr_pool_t *p) +{ + md_acme_acct_t *acct = acme->acct; + md_json_t *jacct; + apr_status_t rv; + + jacct = acct_to_json(acct, p); + + rv = md_store_save(store, p, MD_SG_STAGING, md->name, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 0); + if (APR_SUCCESS == rv) { + rv = md_store_save(store, p, MD_SG_STAGING, md->name, MD_FN_ACCT_KEY, + MD_SV_PKEY, acme->acct_key, 0); + } + return rv; +} + +apr_status_t md_acme_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme, + md_acme_acct_t *acct, md_pkey_t *acct_key) +{ + md_json_t *jacct; + apr_status_t rv; + int i; + const char *id; + + jacct = acct_to_json(acct, p); + id = acct->id; + + if (id) { + rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 0); + } + else { + rv = APR_EAGAIN; + for (i = 0; i < 1000 && APR_SUCCESS != rv; ++i) { + id = mk_acct_id(p, acme, i); + md_json_sets(id, jacct, MD_KEY_ID, NULL); + rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 1); + } + + } + if (APR_SUCCESS == rv) { + acct->id = id; + rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCT_KEY, MD_SV_PKEY, acct_key, 0); + } + return rv; +} + +apr_status_t md_acme_save(md_acme_t *acme, md_store_t *store, apr_pool_t *p) +{ + return md_acme_acct_save(store, p, acme, acme->acct, acme->acct_key); +} + +apr_status_t md_acme_acct_load(md_acme_acct_t **pacct, md_pkey_t **ppkey, + md_store_t *store, md_store_group_t group, + const char *name, apr_pool_t *p) +{ + md_json_t *json; + apr_status_t rv; + + rv = md_store_load_json(store, group, name, MD_FN_ACCOUNT, &json, p); + if (APR_STATUS_IS_ENOENT(rv)) { + goto out; + } + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "error reading account: %s", name); + goto out; + } + + rv = acct_from_json(pacct, json, p); + if (APR_SUCCESS == rv) { + rv = md_store_load(store, group, name, MD_FN_ACCT_KEY, MD_SV_PKEY, (void**)ppkey, p); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "loading key: %s", name); + goto out; + } + } +out: + if (APR_SUCCESS != rv) { + *pacct = NULL; + *ppkey = NULL; + } + return rv; +} + +/**************************************************************************************************/ +/* Lookup */ + +typedef struct { + apr_pool_t *p; + md_acme_t *acme; + const char *id; +} find_ctx; + +static int find_acct(void *baton, const char *name, const char *aspect, + md_store_vtype_t vtype, void *value, apr_pool_t *ptemp) +{ + find_ctx *ctx = baton; + int disabled; + const char *ca_url, *id; + + (void)aspect; + (void)ptemp; + if (MD_SV_JSON == vtype) { + md_json_t *json = value; + + id = md_json_gets(json, MD_KEY_ID, NULL); + disabled = md_json_getb(json, MD_KEY_DISABLED, NULL); + ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL); + + if (!disabled && ca_url && !strcmp(ctx->acme->url, ca_url)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, + "found account %s for %s: %s, disabled=%d, ca-url=%s", + name, ctx->acme->url, id, disabled, ca_url); + ctx->id = id; + return 0; + } + } + return 1; +} + +static apr_status_t acct_find(md_acme_acct_t **pacct, md_pkey_t **ppkey, + md_store_t *store, md_acme_t *acme, apr_pool_t *p) +{ + apr_status_t rv; + find_ctx ctx; + + ctx.p = p; + ctx.acme = acme; + ctx.id = NULL; + + rv = md_store_iter(find_acct, &ctx, store, p, MD_SG_ACCOUNTS, mk_acct_pattern(p, acme), + MD_FN_ACCOUNT, MD_SV_JSON); + if (ctx.id) { + rv = md_acme_acct_load(pacct, ppkey, store, MD_SG_ACCOUNTS, ctx.id, p); + } + else { + *pacct = NULL; + rv = APR_ENOENT; + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "acct_find %s", (*pacct)? (*pacct)->id : "NULL"); + return rv; +} + +/**************************************************************************************************/ +/* Register a new account */ + +typedef struct { + md_acme_t *acme; + apr_pool_t *p; +} acct_ctx_t; + +static apr_status_t on_init_acct_new(md_acme_req_t *req, void *baton) +{ + acct_ctx_t *ctx = baton; + md_json_t *jpayload; + + jpayload = md_json_create(req->p); + md_json_sets("new-reg", jpayload, MD_KEY_RESOURCE, NULL); + md_json_setsa(ctx->acme->acct->contacts, jpayload, MD_KEY_CONTACT, NULL); + if (ctx->acme->acct->agreement) { + md_json_sets(ctx->acme->acct->agreement, jpayload, MD_KEY_AGREEMENT, NULL); + } + + return md_acme_req_body_init(req, jpayload); +} + +static apr_status_t acct_upd(md_acme_t *acme, apr_pool_t *p, + const apr_table_t *hdrs, md_json_t *body, void *baton) +{ + acct_ctx_t *ctx = baton; + apr_status_t rv = APR_SUCCESS; + md_acme_acct_t *acct = acme->acct; + + if (!acct->url) { + const char *location = apr_table_get(hdrs, "location"); + if (!location) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, APR_EINVAL, p, "new acct without location"); + return APR_EINVAL; + } + acct->url = apr_pstrdup(ctx->p, location); + } + if (!acct->tos_required) { + acct->tos_required = md_link_find_relation(hdrs, ctx->p, "terms-of-service"); + if (acct->tos_required) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "server requires agreement to <%s>", acct->tos_required); + } + } + + apr_array_clear(acct->contacts); + md_json_getsa(acct->contacts, body, MD_KEY_CONTACT, NULL); + acct->registration = md_json_clone(ctx->p, body); + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "updated acct %s", acct->url); + return rv; +} + +static apr_status_t acct_register(md_acme_t *acme, apr_pool_t *p, + apr_array_header_t *contacts, const char *agreement) +{ + apr_status_t rv; + md_pkey_t *pkey; + const char *err = NULL, *uri; + md_pkey_spec_t spec; + int i; + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "create new account"); + + if (agreement) { + if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, agreement, &err))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "invalid agreement uri (%s): %s", err, agreement); + goto out; + } + } + for (i = 0; i < contacts->nelts; ++i) { + uri = APR_ARRAY_IDX(contacts, i, const char *); + if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, uri, &err))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "invalid contact uri (%s): %s", err, uri); + goto out; + } + } + + spec.type = MD_PKEY_TYPE_RSA; + spec.params.rsa.bits = MD_ACME_ACCT_PKEY_BITS; + + if (APR_SUCCESS == (rv = md_pkey_gen(&pkey, acme->p, &spec)) + && APR_SUCCESS == (rv = acct_make(&acme->acct, p, acme->url, NULL, contacts))) { + acct_ctx_t ctx; + + acme->acct_key = pkey; + if (agreement) { + acme->acct->agreement = agreement; + } + + ctx.acme = acme; + ctx.p = p; + rv = md_acme_POST(acme, acme->new_reg, on_init_acct_new, acct_upd, NULL, &ctx); + if (APR_SUCCESS == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, + "registered new account %s", acme->acct->url); + } + } + +out: + if (APR_SUCCESS != rv && acme->acct) { + acme->acct = NULL; + } + return rv; +} + +/**************************************************************************************************/ +/* acct validation */ + +static apr_status_t on_init_acct_valid(md_acme_req_t *req, void *baton) +{ + md_json_t *jpayload; + + (void)baton; + jpayload = md_json_create(req->p); + md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL); + + return md_acme_req_body_init(req, jpayload); +} + +static apr_status_t acct_valid(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, + md_json_t *body, void *baton) +{ + md_acme_acct_t *acct = acme->acct; + apr_status_t rv = APR_SUCCESS; + const char *body_str; + const char *tos_required; + + (void)p; + (void)baton; + apr_array_clear(acct->contacts); + md_json_getsa(acct->contacts, body, MD_KEY_CONTACT, NULL); + acct->registration = md_json_clone(acme->p, body); + + body_str = md_json_writep(body, acme->p, MD_JSON_FMT_INDENT); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, "validate acct %s: %s", + acct->url, body_str ? body_str : "<failed to serialize!>"); + + acct->agreement = md_json_gets(acct->registration, MD_KEY_AGREEMENT, NULL); + tos_required = md_link_find_relation(hdrs, acme->p, "terms-of-service"); + + if (tos_required) { + if (!acct->agreement || strcmp(tos_required, acct->agreement)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, + "needs to agree to terms-of-service '%s', " + "has already agreed to '%s'", + tos_required, acct->agreement); + } + acct->tos_required = tos_required; + } + + return rv; +} + +static apr_status_t md_acme_validate_acct(md_acme_t *acme) +{ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "acct validation"); + if (!acme->acct) { + return APR_EINVAL; + } + return md_acme_POST(acme, acme->acct->url, on_init_acct_valid, acct_valid, NULL, NULL); +} + +/**************************************************************************************************/ +/* account setup */ + +static apr_status_t acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t *p) +{ + apr_status_t rv; + + if (APR_SUCCESS != (rv = md_acme_validate_acct(acme))) { + if (acme->acct && (APR_ENOENT == rv || APR_EACCES == rv)) { + if (!acme->acct->disabled) { + acme->acct->disabled = 1; + if (store) { + md_acme_save(acme, store, p); + } + } + acme->acct = NULL; + acme->acct_key = NULL; + rv = APR_ENOENT; + } + } + return rv; +} + +apr_status_t md_acme_use_acct(md_acme_t *acme, md_store_t *store, + apr_pool_t *p, const char *acct_id) +{ + md_acme_acct_t *acct; + md_pkey_t *pkey; + apr_status_t rv; + + if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey, + store, MD_SG_ACCOUNTS, acct_id, acme->p))) { + if (acct->ca_url && !strcmp(acct->ca_url, acme->url)) { + acme->acct = acct; + acme->acct_key = pkey; + rv = acct_validate(acme, store, p); + } + else { + /* account is from a nother server or, more likely, from another + * protocol endpoint on the same server */ + rv = APR_ENOENT; + } + } + return rv; +} + +apr_status_t md_acme_use_acct_staged(md_acme_t *acme, struct md_store_t *store, + md_t *md, apr_pool_t *p) +{ + md_acme_acct_t *acct; + md_pkey_t *pkey; + apr_status_t rv; + + if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey, + store, MD_SG_STAGING, md->name, acme->p))) { + acme->acct = acct; + acme->acct_key = pkey; + rv = acct_validate(acme, NULL, p); + } + return rv; +} + +const char *md_acme_get_acct_id(md_acme_t *acme) +{ + return acme->acct? acme->acct->id : NULL; +} + +const char *md_acme_get_agreement(md_acme_t *acme) +{ + return acme->acct? acme->acct->agreement : NULL; +} + +apr_status_t md_acme_find_acct(md_acme_t *acme, md_store_t *store, apr_pool_t *p) +{ + md_acme_acct_t *acct; + md_pkey_t *pkey; + apr_status_t rv; + + while (APR_SUCCESS == acct_find(&acct, &pkey, store, acme, acme->p)) { + acme->acct = acct; + acme->acct_key = pkey; + rv = acct_validate(acme, store, p); + + if (APR_SUCCESS == rv) { + return rv; + } + else { + acme->acct = NULL; + acme->acct_key = NULL; + if (!APR_STATUS_IS_ENOENT(rv)) { + /* encountered error with server */ + return rv; + } + } + } + return APR_ENOENT; +} + +apr_status_t md_acme_create_acct(md_acme_t *acme, apr_pool_t *p, apr_array_header_t *contacts, + const char *agreement) +{ + return acct_register(acme, p, contacts, agreement); +} + +/**************************************************************************************************/ +/* Delete the account */ + +apr_status_t md_acme_unstore_acct(md_store_t *store, apr_pool_t *p, const char *acct_id) +{ + apr_status_t rv = APR_SUCCESS; + + rv = md_store_remove(store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCOUNT, p, 1); + if (APR_SUCCESS == rv) { + md_store_remove(store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCT_KEY, p, 1); + } + return rv; +} + +static apr_status_t on_init_acct_del(md_acme_req_t *req, void *baton) +{ + md_json_t *jpayload; + + (void)baton; + jpayload = md_json_create(req->p); + md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL); + md_json_setb(1, jpayload, "delete", NULL); + + return md_acme_req_body_init(req, jpayload); +} + +static apr_status_t acct_del(md_acme_t *acme, apr_pool_t *p, + const apr_table_t *hdrs, md_json_t *body, void *baton) +{ + md_store_t *store = baton; + apr_status_t rv = APR_SUCCESS; + + (void)hdrs; + (void)body; + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, "deleted account %s", acme->acct->url); + if (store) { + rv = md_acme_unstore_acct(store, p, acme->acct->id); + acme->acct = NULL; + acme->acct_key = NULL; + } + return rv; +} + +apr_status_t md_acme_delete_acct(md_acme_t *acme, md_store_t *store, apr_pool_t *p) +{ + md_acme_acct_t *acct = acme->acct; + + (void)p; + if (!acct) { + return APR_EINVAL; + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "delete account %s from %s", + acct->url, acct->ca_url); + return md_acme_POST(acme, acct->url, on_init_acct_del, acct_del, NULL, store); +} + +/**************************************************************************************************/ +/* terms-of-service */ + +static apr_status_t on_init_agree_tos(md_acme_req_t *req, void *baton) +{ + acct_ctx_t *ctx = baton; + md_json_t *jpayload; + + jpayload = md_json_create(req->p); + md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL); + md_json_sets(ctx->acme->acct->agreement, jpayload, MD_KEY_AGREEMENT, NULL); + + return md_acme_req_body_init(req, jpayload); +} + +apr_status_t md_acme_agree(md_acme_t *acme, apr_pool_t *p, const char *agreement) +{ + acct_ctx_t ctx; + + acme->acct->agreement = agreement; + ctx.acme = acme; + ctx.p = p; + return md_acme_POST(acme, acme->acct->url, on_init_agree_tos, acct_upd, NULL, &ctx); +} + +static int agreement_required(md_acme_acct_t *acct) +{ + /* We used to really check if the account agreement and the one + * indicated as valid are the very same: + * return (!acct->agreement + * || (acct->tos_required && strcmp(acct->tos_required, acct->agreement))); + * However, LE is happy if the account has agreed to a ToS in the past and + * does not required a renewed acceptance. + */ + return !acct->agreement; +} + +apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p, + const char *agreement, const char **prequired) +{ + apr_status_t rv = APR_SUCCESS; + + /* Check if (correct) Terms-of-Service for account were accepted */ + *prequired = NULL; + if (agreement_required(acme->acct)) { + const char *tos = acme->acct->tos_required; + if (!tos) { + if (APR_SUCCESS != (rv = md_acme_validate_acct(acme))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, + "validate for account %s", acme->acct->id); + return rv; + } + tos = acme->acct->tos_required; + if (!tos) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, "unknown terms-of-service " + "required after validation of account %s", acme->acct->id); + return APR_EGENERAL; + } + } + + if (acme->acct->agreement && !strcmp(tos, acme->acct->agreement)) { + rv = md_acme_agree(acme, p, tos); + } + else if (agreement && !strcmp(tos, agreement)) { + rv = md_acme_agree(acme, p, tos); + } + else { + *prequired = apr_pstrdup(p, tos); + rv = APR_INCOMPLETE; + } + } + return rv; +} diff --git a/modules/md/md_acme_acct.h b/modules/md/md_acme_acct.h new file mode 100644 index 0000000..e200da3 --- /dev/null +++ b/modules/md/md_acme_acct.h @@ -0,0 +1,49 @@ +/* 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. + */ + +#ifndef mod_md_md_acme_acct_h +#define mod_md_md_acme_acct_h + +struct md_acme_req; +struct md_json_t; +struct md_pkey_t; + + +/** + * An ACME account at an ACME server. + */ +typedef struct md_acme_acct_t md_acme_acct_t; + +struct md_acme_acct_t { + const char *id; /* short, unique id for the account */ + const char *url; /* url of the account, once registered */ + const char *ca_url; /* url of the ACME protocol endpoint */ + apr_array_header_t *contacts; /* list of contact uris, e.g. mailto:xxx */ + const char *tos_required; /* terms of service asked for by CA */ + const char *agreement; /* terms of service agreed to by user */ + + struct md_json_t *registration; /* data from server registration */ + int disabled; +}; + +#define MD_FN_ACCOUNT "account.json" +#define MD_FN_ACCT_KEY "account.pem" + +/* ACME account private keys are always RSA and have that many bits. Since accounts + * are expected to live long, better err on the safe side. */ +#define MD_ACME_ACCT_PKEY_BITS 3072 + +#endif /* md_acme_acct_h */ diff --git a/modules/md/md_acme_authz.c b/modules/md/md_acme_authz.c new file mode 100644 index 0000000..2b5cbdc --- /dev/null +++ b/modules/md/md_acme_authz.c @@ -0,0 +1,723 @@ +/* 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. + */ + +#include <assert.h> +#include <stdio.h> + +#include <apr_lib.h> +#include <apr_buckets.h> +#include <apr_file_info.h> +#include <apr_file_io.h> +#include <apr_fnmatch.h> +#include <apr_hash.h> +#include <apr_strings.h> +#include <apr_tables.h> + +#include "md.h" +#include "md_crypt.h" +#include "md_json.h" +#include "md_http.h" +#include "md_log.h" +#include "md_jws.h" +#include "md_store.h" +#include "md_util.h" + +#include "md_acme.h" +#include "md_acme_authz.h" + +md_acme_authz_t *md_acme_authz_create(apr_pool_t *p) +{ + md_acme_authz_t *authz; + authz = apr_pcalloc(p, sizeof(*authz)); + + return authz; +} + +md_acme_authz_set_t *md_acme_authz_set_create(apr_pool_t *p) +{ + md_acme_authz_set_t *authz_set; + + authz_set = apr_pcalloc(p, sizeof(*authz_set)); + authz_set->authzs = apr_array_make(p, 5, sizeof(md_acme_authz_t *)); + + return authz_set; +} + +md_acme_authz_t *md_acme_authz_set_get(md_acme_authz_set_t *set, const char *domain) +{ + md_acme_authz_t *authz; + int i; + + assert(domain); + for (i = 0; i < set->authzs->nelts; ++i) { + authz = APR_ARRAY_IDX(set->authzs, i, md_acme_authz_t *); + if (!apr_strnatcasecmp(domain, authz->domain)) { + return authz; + } + } + return NULL; +} + +apr_status_t md_acme_authz_set_add(md_acme_authz_set_t *set, md_acme_authz_t *authz) +{ + md_acme_authz_t *existing; + + assert(authz->domain); + if (NULL != (existing = md_acme_authz_set_get(set, authz->domain))) { + return APR_EINVAL; + } + APR_ARRAY_PUSH(set->authzs, md_acme_authz_t*) = authz; + return APR_SUCCESS; +} + +apr_status_t md_acme_authz_set_remove(md_acme_authz_set_t *set, const char *domain) +{ + md_acme_authz_t *authz; + int i; + + assert(domain); + for (i = 0; i < set->authzs->nelts; ++i) { + authz = APR_ARRAY_IDX(set->authzs, i, md_acme_authz_t *); + if (!apr_strnatcasecmp(domain, authz->domain)) { + int n = i + 1; + if (n < set->authzs->nelts) { + void **elems = (void **)set->authzs->elts; + memmove(elems + i, elems + n, (size_t)(set->authzs->nelts - n) * sizeof(*elems)); + } + --set->authzs->nelts; + return APR_SUCCESS; + } + } + return APR_ENOENT; +} + +/**************************************************************************************************/ +/* Register a new authorization */ + +typedef struct { + size_t index; + const char *type; + const char *uri; + const char *token; + const char *key_authz; +} md_acme_authz_cha_t; + +typedef struct { + apr_pool_t *p; + md_acme_t *acme; + const char *domain; + md_acme_authz_t *authz; + md_acme_authz_cha_t *challenge; +} authz_req_ctx; + +static void authz_req_ctx_init(authz_req_ctx *ctx, md_acme_t *acme, + const char *domain, md_acme_authz_t *authz, apr_pool_t *p) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->p = p; + ctx->acme = acme; + ctx->domain = domain; + ctx->authz = authz; +} + +static apr_status_t on_init_authz(md_acme_req_t *req, void *baton) +{ + authz_req_ctx *ctx = baton; + md_json_t *jpayload; + + jpayload = md_json_create(req->p); + md_json_sets("new-authz", jpayload, MD_KEY_RESOURCE, NULL); + md_json_sets("dns", jpayload, MD_KEY_IDENTIFIER, MD_KEY_TYPE, NULL); + md_json_sets(ctx->domain, jpayload, MD_KEY_IDENTIFIER, MD_KEY_VALUE, NULL); + + return md_acme_req_body_init(req, jpayload); +} + +static apr_status_t authz_created(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, + md_json_t *body, void *baton) +{ + authz_req_ctx *ctx = baton; + const char *location = apr_table_get(hdrs, "location"); + apr_status_t rv = APR_SUCCESS; + + (void)acme; + (void)p; + if (location) { + ctx->authz = md_acme_authz_create(ctx->p); + ctx->authz->domain = apr_pstrdup(ctx->p, ctx->domain); + ctx->authz->location = apr_pstrdup(ctx->p, location); + ctx->authz->resource = md_json_clone(ctx->p, body); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ctx->p, "authz_new at %s", location); + } + else { + rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, ctx->p, "new authz, no location header"); + } + return rv; +} + +apr_status_t md_acme_authz_register(struct md_acme_authz_t **pauthz, md_acme_t *acme, + md_store_t *store, const char *domain, apr_pool_t *p) +{ + apr_status_t rv; + authz_req_ctx ctx; + + (void)store; + authz_req_ctx_init(&ctx, acme, domain, NULL, p); + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "create new authz"); + rv = md_acme_POST(acme, acme->new_authz, on_init_authz, authz_created, NULL, &ctx); + + *pauthz = (APR_SUCCESS == rv)? ctx.authz : NULL; + return rv; +} + +/**************************************************************************************************/ +/* Update an existing authorization */ + +apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, + md_store_t *store, apr_pool_t *p) +{ + md_json_t *json; + const char *s, *err; + md_log_level_t log_level; + apr_status_t rv; + MD_CHK_VARS; + + (void)store; + assert(acme); + assert(acme->http); + assert(authz); + assert(authz->location); + + authz->state = MD_ACME_AUTHZ_S_UNKNOWN; + json = NULL; + err = "unable to parse response"; + log_level = MD_LOG_ERR; + + if (MD_OK(md_acme_get_json(&json, acme, authz->location, p)) + && (s = md_json_gets(json, MD_KEY_IDENTIFIER, MD_KEY_TYPE, NULL)) + && !strcmp(s, "dns") + && (s = md_json_gets(json, MD_KEY_IDENTIFIER, MD_KEY_VALUE, NULL)) + && !strcmp(s, authz->domain) + && (s = md_json_gets(json, MD_KEY_STATUS, NULL))) { + + authz->resource = json; + if (!strcmp(s, "pending")) { + authz->state = MD_ACME_AUTHZ_S_PENDING; + err = "challenge 'pending'"; + log_level = MD_LOG_DEBUG; + } + else if (!strcmp(s, "valid")) { + authz->state = MD_ACME_AUTHZ_S_VALID; + err = "challenge 'valid'"; + log_level = MD_LOG_DEBUG; + } + else if (!strcmp(s, "invalid")) { + authz->state = MD_ACME_AUTHZ_S_INVALID; + err = "challenge 'invalid'"; + } + } + + if (json && authz->state == MD_ACME_AUTHZ_S_UNKNOWN) { + err = "unable to understand response"; + rv = APR_EINVAL; + } + + if (md_log_is_level(p, log_level)) { + md_log_perror(MD_LOG_MARK, log_level, rv, p, "ACME server authz: %s for %s at %s. " + "Exact repsonse was: %s", err? err : "", authz->domain, authz->location, + json? md_json_writep(json, p, MD_JSON_FMT_COMPACT) : "not available"); + } + + return rv; +} + +/**************************************************************************************************/ +/* response to a challenge */ + +static md_acme_authz_cha_t *cha_from_json(apr_pool_t *p, size_t index, md_json_t *json) +{ + md_acme_authz_cha_t * cha; + + cha = apr_pcalloc(p, sizeof(*cha)); + cha->index = index; + cha->type = md_json_dups(p, json, MD_KEY_TYPE, NULL); + cha->uri = md_json_dups(p, json, MD_KEY_URI, NULL); + cha->token = md_json_dups(p, json, MD_KEY_TOKEN, NULL); + cha->key_authz = md_json_dups(p, json, MD_KEY_KEYAUTHZ, NULL); + + return cha; +} + +static apr_status_t on_init_authz_resp(md_acme_req_t *req, void *baton) +{ + authz_req_ctx *ctx = baton; + md_json_t *jpayload; + + jpayload = md_json_create(req->p); + md_json_sets("challenge", jpayload, MD_KEY_RESOURCE, NULL); + md_json_sets(ctx->challenge->key_authz, jpayload, MD_KEY_KEYAUTHZ, NULL); + + return md_acme_req_body_init(req, jpayload); +} + +static apr_status_t authz_http_set(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, + md_json_t *body, void *baton) +{ + authz_req_ctx *ctx = baton; + + (void)acme; + (void)p; + (void)hdrs; + (void)body; + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "updated authz %s", ctx->authz->location); + return APR_SUCCESS; +} + +static apr_status_t setup_key_authz(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, + md_acme_t *acme, apr_pool_t *p, int *pchanged) +{ + const char *thumb64, *key_authz; + apr_status_t rv; + MD_CHK_VARS; + + (void)authz; + assert(cha); + assert(cha->token); + + *pchanged = 0; + if (MD_OK(md_jws_pkey_thumb(&thumb64, p, acme->acct_key))) { + key_authz = apr_psprintf(p, "%s.%s", cha->token, thumb64); + if (cha->key_authz) { + if (strcmp(key_authz, cha->key_authz)) { + /* Hu? Did the account change key? */ + cha->key_authz = NULL; + } + } + if (!cha->key_authz) { + cha->key_authz = key_authz; + *pchanged = 1; + } + } + return rv; +} + +static apr_status_t cha_http_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, + md_acme_t *acme, md_store_t *store, + md_pkey_spec_t *key_spec, apr_pool_t *p) +{ + const char *data; + apr_status_t rv; + int notify_server; + MD_CHK_VARS; + + (void)key_spec; + if (!MD_OK(setup_key_authz(cha, authz, acme, p, ¬ify_server))) { + goto out; + } + + rv = md_store_load(store, MD_SG_CHALLENGES, authz->domain, MD_FN_HTTP01, + MD_SV_TEXT, (void**)&data, p); + if ((APR_SUCCESS == rv && strcmp(cha->key_authz, data)) || APR_STATUS_IS_ENOENT(rv)) { + rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, MD_FN_HTTP01, + MD_SV_TEXT, (void*)cha->key_authz, 0); + authz->dir = authz->domain; + notify_server = 1; + } + + if (APR_SUCCESS == rv && notify_server) { + authz_req_ctx ctx; + + /* challenge is setup or was changed from previous data, tell ACME server + * so it may (re)try verification */ + authz_req_ctx_init(&ctx, acme, NULL, authz, p); + ctx.challenge = cha; + rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, &ctx); + } +out: + return rv; +} + +static apr_status_t setup_cha_dns(const char **pdns, md_acme_authz_cha_t *cha, apr_pool_t *p) +{ + const char *dhex; + char *dns; + apr_size_t dhex_len; + apr_status_t rv; + + rv = md_crypt_sha256_digest_hex(&dhex, p, cha->key_authz, strlen(cha->key_authz)); + if (APR_SUCCESS == rv) { + dhex = md_util_str_tolower((char*)dhex); + dhex_len = strlen(dhex); + assert(dhex_len > 32); + dns = apr_pcalloc(p, dhex_len + 1 + sizeof(MD_TLSSNI01_DNS_SUFFIX)); + strncpy(dns, dhex, 32); + dns[32] = '.'; + strncpy(dns+33, dhex+32, dhex_len-32); + memcpy(dns+(dhex_len+1), MD_TLSSNI01_DNS_SUFFIX, sizeof(MD_TLSSNI01_DNS_SUFFIX)); + } + *pdns = (APR_SUCCESS == rv)? dns : NULL; + return rv; +} + +static apr_status_t cha_tls_sni_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, + md_acme_t *acme, md_store_t *store, + md_pkey_spec_t *key_spec, apr_pool_t *p) +{ + md_cert_t *cha_cert; + md_pkey_t *cha_key; + const char *cha_dns; + apr_status_t rv; + int notify_server; + apr_array_header_t *domains; + MD_CHK_VARS; + + if ( !MD_OK(setup_key_authz(cha, authz, acme, p, ¬ify_server)) + || !MD_OK(setup_cha_dns(&cha_dns, cha, p))) { + goto out; + } + + rv = md_store_load(store, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_CERT, + MD_SV_CERT, (void**)&cha_cert, p); + if ((APR_SUCCESS == rv && !md_cert_covers_domain(cha_cert, cha_dns)) + || APR_STATUS_IS_ENOENT(rv)) { + + if (APR_SUCCESS != (rv = md_pkey_gen(&cha_key, p, key_spec))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create tls-sni-01 challenge key", + authz->domain); + goto out; + } + + /* setup a certificate containing the challenge dns */ + domains = apr_array_make(p, 5, sizeof(const char*)); + APR_ARRAY_PUSH(domains, const char*) = cha_dns; + if (!MD_OK(md_cert_self_sign(&cha_cert, authz->domain, domains, cha_key, + apr_time_from_sec(7 * MD_SECS_PER_DAY), p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: setup self signed cert for %s", + authz->domain, cha_dns); + goto out; + } + + if (MD_OK(md_store_save(store, p, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_PKEY, + MD_SV_PKEY, (void*)cha_key, 0))) { + rv = md_store_save(store, p, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_CERT, + MD_SV_CERT, (void*)cha_cert, 0); + } + authz->dir = cha_dns; + notify_server = 1; + } + + if (APR_SUCCESS == rv && notify_server) { + authz_req_ctx ctx; + + /* challenge is setup or was changed from previous data, tell ACME server + * so it may (re)try verification */ + authz_req_ctx_init(&ctx, acme, NULL, authz, p); + ctx.challenge = cha; + rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, &ctx); + } +out: + return rv; +} + +typedef apr_status_t cha_starter(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, + md_acme_t *acme, md_store_t *store, + md_pkey_spec_t *key_spec, apr_pool_t *p); + +typedef struct { + const char *name; + cha_starter *start; +} cha_type; + +static const cha_type CHA_TYPES[] = { + { MD_AUTHZ_TYPE_HTTP01, cha_http_01_setup }, + { MD_AUTHZ_TYPE_TLSSNI01, cha_tls_sni_01_setup }, +}; +static const apr_size_t CHA_TYPES_LEN = (sizeof(CHA_TYPES)/sizeof(CHA_TYPES[0])); + +typedef struct { + apr_pool_t *p; + const char *type; + md_acme_authz_cha_t *accepted; + apr_array_header_t *offered; +} cha_find_ctx; + +static apr_status_t collect_offered(void *baton, size_t index, md_json_t *json) +{ + cha_find_ctx *ctx = baton; + const char *ctype; + + (void)index; + if ((ctype = md_json_gets(json, MD_KEY_TYPE, NULL))) { + APR_ARRAY_PUSH(ctx->offered, const char*) = apr_pstrdup(ctx->p, ctype); + } + return 1; +} + +static apr_status_t find_type(void *baton, size_t index, md_json_t *json) +{ + cha_find_ctx *ctx = baton; + + const char *ctype = md_json_gets(json, MD_KEY_TYPE, NULL); + if (ctype && !apr_strnatcasecmp(ctx->type, ctype)) { + ctx->accepted = cha_from_json(ctx->p, index, json); + return 0; + } + return 1; +} + +apr_status_t md_acme_authz_respond(md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store, + apr_array_header_t *challenges, + md_pkey_spec_t *key_spec, apr_pool_t *p) +{ + apr_status_t rv; + int i; + cha_find_ctx fctx; + + assert(acme); + assert(authz); + assert(authz->resource); + + fctx.p = p; + fctx.accepted = NULL; + + /* Look in the order challenge types are defined */ + for (i = 0; i < challenges->nelts && !fctx.accepted; ++i) { + fctx.type = APR_ARRAY_IDX(challenges, i, const char *); + md_json_itera(find_type, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL); + } + + if (!fctx.accepted) { + rv = APR_EINVAL; + fctx.offered = apr_array_make(p, 5, sizeof(const char*)); + md_json_itera(collect_offered, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL); + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, + "%s: the server offers no ACME challenge that is configured " + "for this MD. The server offered '%s' and available for this " + "MD are: '%s' (via %s).", + authz->domain, + apr_array_pstrcat(p, fctx.offered, ' '), + apr_array_pstrcat(p, challenges, ' '), + authz->location); + return rv; + } + + for (i = 0; i < (int)CHA_TYPES_LEN; ++i) { + if (!apr_strnatcasecmp(CHA_TYPES[i].name, fctx.accepted->type)) { + return CHA_TYPES[i].start(fctx.accepted, authz, acme, store, key_spec, p); + } + } + + rv = APR_ENOTIMPL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, + "%s: no implementation found for challenge '%s'", + authz->domain, fctx.accepted->type); + return rv; +} + +/**************************************************************************************************/ +/* Delete an existing authz resource */ + +typedef struct { + apr_pool_t *p; + md_acme_authz_t *authz; +} del_ctx; + +static apr_status_t on_init_authz_del(md_acme_req_t *req, void *baton) +{ + md_json_t *jpayload; + + (void)baton; + jpayload = md_json_create(req->p); + md_json_sets("deactivated", jpayload, MD_KEY_STATUS, NULL); + + return md_acme_req_body_init(req, jpayload); +} + +static apr_status_t authz_del(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, + md_json_t *body, void *baton) +{ + authz_req_ctx *ctx = baton; + + (void)p; + (void)body; + (void)hdrs; + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "deleted authz %s", ctx->authz->location); + acme->acct = NULL; + return APR_SUCCESS; +} + +apr_status_t md_acme_authz_del(md_acme_authz_t *authz, md_acme_t *acme, + md_store_t *store, apr_pool_t *p) +{ + authz_req_ctx ctx; + + (void)store; + ctx.p = p; + ctx.authz = authz; + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "delete authz for %s from %s", + authz->domain, authz->location); + return md_acme_POST(acme, authz->location, on_init_authz_del, authz_del, NULL, &ctx); +} + +/**************************************************************************************************/ +/* authz conversion */ + +md_json_t *md_acme_authz_to_json(md_acme_authz_t *a, apr_pool_t *p) +{ + md_json_t *json = md_json_create(p); + if (json) { + md_json_sets(a->domain, json, MD_KEY_DOMAIN, NULL); + md_json_sets(a->location, json, MD_KEY_LOCATION, NULL); + md_json_sets(a->dir, json, MD_KEY_DIR, NULL); + md_json_setl(a->state, json, MD_KEY_STATE, NULL); + return json; + } + return NULL; +} + +md_acme_authz_t *md_acme_authz_from_json(struct md_json_t *json, apr_pool_t *p) +{ + md_acme_authz_t *authz = md_acme_authz_create(p); + if (authz) { + authz->domain = md_json_dups(p, json, MD_KEY_DOMAIN, NULL); + authz->location = md_json_dups(p, json, MD_KEY_LOCATION, NULL); + authz->dir = md_json_dups(p, json, MD_KEY_DIR, NULL); + authz->state = (md_acme_authz_state_t)md_json_getl(json, MD_KEY_STATE, NULL); + return authz; + } + return NULL; +} + +/**************************************************************************************************/ +/* authz_set conversion */ + +#define MD_KEY_ACCOUNT "account" +#define MD_KEY_AUTHZS "authorizations" + +static apr_status_t authz_to_json(void *value, md_json_t *json, apr_pool_t *p, void *baton) +{ + (void)baton; + return md_json_setj(md_acme_authz_to_json(value, p), json, NULL); +} + +static apr_status_t authz_from_json(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton) +{ + (void)baton; + *pvalue = md_acme_authz_from_json(json, p); + return (*pvalue)? APR_SUCCESS : APR_EINVAL; +} + +md_json_t *md_acme_authz_set_to_json(md_acme_authz_set_t *set, apr_pool_t *p) +{ + md_json_t *json = md_json_create(p); + if (json) { + md_json_seta(set->authzs, authz_to_json, NULL, json, MD_KEY_AUTHZS, NULL); + return json; + } + return NULL; +} + +md_acme_authz_set_t *md_acme_authz_set_from_json(md_json_t *json, apr_pool_t *p) +{ + md_acme_authz_set_t *set = md_acme_authz_set_create(p); + if (set) { + md_json_geta(set->authzs, authz_from_json, NULL, json, MD_KEY_AUTHZS, NULL); + return set; + } + return NULL; +} + +/**************************************************************************************************/ +/* persistence */ + +apr_status_t md_acme_authz_set_load(struct md_store_t *store, md_store_group_t group, + const char *md_name, md_acme_authz_set_t **pauthz_set, + apr_pool_t *p) +{ + apr_status_t rv; + md_json_t *json; + md_acme_authz_set_t *authz_set; + + rv = md_store_load_json(store, group, md_name, MD_FN_AUTHZ, &json, p); + if (APR_SUCCESS == rv) { + authz_set = md_acme_authz_set_from_json(json, p); + } + *pauthz_set = (APR_SUCCESS == rv)? authz_set : NULL; + return rv; +} + +static apr_status_t p_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_t *store = baton; + md_json_t *json; + md_store_group_t group; + md_acme_authz_set_t *set; + const char *md_name; + int create; + + (void)p; + group = (md_store_group_t)va_arg(ap, int); + md_name = va_arg(ap, const char *); + set = va_arg(ap, md_acme_authz_set_t *); + create = va_arg(ap, int); + + json = md_acme_authz_set_to_json(set, ptemp); + assert(json); + return md_store_save_json(store, ptemp, group, md_name, MD_FN_AUTHZ, json, create); +} + +apr_status_t md_acme_authz_set_save(struct md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *md_name, + md_acme_authz_set_t *authz_set, int create) +{ + return md_util_pool_vdo(p_save, store, p, group, md_name, authz_set, create, NULL); +} + +static apr_status_t p_purge(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_t *store = baton; + md_acme_authz_set_t *authz_set; + const md_acme_authz_t *authz; + md_store_group_t group; + const char *md_name; + int i; + + group = (md_store_group_t)va_arg(ap, int); + md_name = va_arg(ap, const char *); + + if (APR_SUCCESS == md_acme_authz_set_load(store, group, md_name, &authz_set, p)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz_set loaded for %s", md_name); + for (i = 0; i < authz_set->authzs->nelts; ++i) { + authz = APR_ARRAY_IDX(authz_set->authzs, i, const md_acme_authz_t*); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz check %s", authz->domain); + if (authz->dir) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz purge %s", authz->dir); + md_store_purge(store, p, MD_SG_CHALLENGES, authz->dir); + } + } + } + return md_store_remove(store, group, md_name, MD_FN_AUTHZ, ptemp, 1); +} + +apr_status_t md_acme_authz_set_purge(md_store_t *store, md_store_group_t group, + apr_pool_t *p, const char *md_name) +{ + return md_util_pool_vdo(p_purge, store, p, group, md_name, NULL); +} + diff --git a/modules/md/md_acme_authz.h b/modules/md/md_acme_authz.h new file mode 100644 index 0000000..aa33f23 --- /dev/null +++ b/modules/md/md_acme_authz.h @@ -0,0 +1,104 @@ +/* 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. + */ + +#ifndef mod_md_md_acme_authz_h +#define mod_md_md_acme_authz_h + +struct apr_array_header_t; +struct md_acme_t; +struct md_acme_acct_t; +struct md_json_t; +struct md_store_t; +struct md_pkey_spec_t; + +typedef struct md_acme_challenge_t md_acme_challenge_t; + +/**************************************************************************************************/ +/* authorization request for a specific domain name */ + +#define MD_AUTHZ_TYPE_HTTP01 "http-01" +#define MD_AUTHZ_TYPE_TLSSNI01 "tls-sni-01" + +typedef enum { + MD_ACME_AUTHZ_S_UNKNOWN, + MD_ACME_AUTHZ_S_PENDING, + MD_ACME_AUTHZ_S_VALID, + MD_ACME_AUTHZ_S_INVALID, +} md_acme_authz_state_t; + +typedef struct md_acme_authz_t md_acme_authz_t; + +struct md_acme_authz_t { + const char *domain; + const char *location; + const char *dir; + md_acme_authz_state_t state; + apr_time_t expires; + struct md_json_t *resource; +}; + +#define MD_FN_HTTP01 "acme-http-01.txt" +#define MD_FN_TLSSNI01_CERT "acme-tls-sni-01.cert.pem" +#define MD_FN_TLSSNI01_PKEY "acme-tls-sni-01.key.pem" +#define MD_FN_AUTHZ "authz.json" + + +md_acme_authz_t *md_acme_authz_create(apr_pool_t *p); + +struct md_json_t *md_acme_authz_to_json(md_acme_authz_t *a, apr_pool_t *p); +md_acme_authz_t *md_acme_authz_from_json(struct md_json_t *json, apr_pool_t *p); + +/* authz interaction with ACME server */ +apr_status_t md_acme_authz_register(struct md_acme_authz_t **pauthz, struct md_acme_t *acme, + struct md_store_t *store, const char *domain, apr_pool_t *p); + +apr_status_t md_acme_authz_update(md_acme_authz_t *authz, struct md_acme_t *acme, + struct md_store_t *store, apr_pool_t *p); + +apr_status_t md_acme_authz_respond(md_acme_authz_t *authz, struct md_acme_t *acme, + struct md_store_t *store, apr_array_header_t *challenges, + struct md_pkey_spec_t *key_spec, apr_pool_t *p); +apr_status_t md_acme_authz_del(md_acme_authz_t *authz, struct md_acme_t *acme, + struct md_store_t *store, apr_pool_t *p); + +/**************************************************************************************************/ +/* set of authz data for a managed domain */ + +typedef struct md_acme_authz_set_t md_acme_authz_set_t; + +struct md_acme_authz_set_t { + struct apr_array_header_t *authzs; +}; + +md_acme_authz_set_t *md_acme_authz_set_create(apr_pool_t *p); +md_acme_authz_t *md_acme_authz_set_get(md_acme_authz_set_t *set, const char *domain); +apr_status_t md_acme_authz_set_add(md_acme_authz_set_t *set, md_acme_authz_t *authz); +apr_status_t md_acme_authz_set_remove(md_acme_authz_set_t *set, const char *domain); + +struct md_json_t *md_acme_authz_set_to_json(md_acme_authz_set_t *set, apr_pool_t *p); +md_acme_authz_set_t *md_acme_authz_set_from_json(struct md_json_t *json, apr_pool_t *p); + +apr_status_t md_acme_authz_set_load(struct md_store_t *store, md_store_group_t group, + const char *md_name, md_acme_authz_set_t **pauthz_set, + apr_pool_t *p); +apr_status_t md_acme_authz_set_save(struct md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *md_name, + md_acme_authz_set_t *authz_set, int create); + +apr_status_t md_acme_authz_set_purge(struct md_store_t *store, md_store_group_t group, + apr_pool_t *p, const char *md_name); + +#endif /* md_acme_authz_h */ diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c new file mode 100644 index 0000000..ba4e865 --- /dev/null +++ b/modules/md/md_acme_drive.c @@ -0,0 +1,1024 @@ +/* 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. + */ + +#include <assert.h> +#include <stdlib.h> + +#include <apr_lib.h> +#include <apr_strings.h> +#include <apr_buckets.h> +#include <apr_hash.h> +#include <apr_uri.h> + +#include "md.h" +#include "md_crypt.h" +#include "md_json.h" +#include "md_jws.h" +#include "md_http.h" +#include "md_log.h" +#include "md_reg.h" +#include "md_store.h" +#include "md_util.h" + +#include "md_acme.h" +#include "md_acme_acct.h" +#include "md_acme_authz.h" + +typedef struct { + md_proto_driver_t *driver; + + const char *phase; + int complete; + + md_pkey_t *privkey; /* the new private key */ + apr_array_header_t *pubcert; /* the new certificate + chain certs */ + + md_cert_t *cert; /* the new certificate */ + apr_array_header_t *chain; /* the chain certificates */ + const char *next_up_link; /* where the next chain cert is */ + + md_acme_t *acme; + md_t *md; + const md_creds_t *ncreds; + + apr_array_header_t *ca_challenges; + md_acme_authz_set_t *authz_set; + apr_interval_time_t authz_monitor_timeout; + + const char *csr_der_64; + apr_interval_time_t cert_poll_timeout; + +} md_acme_driver_t; + +/**************************************************************************************************/ +/* account setup */ + +static apr_status_t ad_set_acct(md_proto_driver_t *d) +{ + md_acme_driver_t *ad = d->baton; + md_t *md = ad->md; + apr_status_t rv = APR_SUCCESS; + int update = 0, acct_installed = 0; + + ad->phase = "setup acme"; + if (!ad->acme + && APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, md->ca_url, d->proxy_url))) { + goto out; + } + + ad->phase = "choose account"; + /* Do we have a staged (modified) account? */ + if (APR_SUCCESS == (rv = md_acme_use_acct_staged(ad->acme, d->store, md, d->p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "re-using staged account"); + md->ca_account = MD_ACME_ACCT_STAGED; + acct_installed = 1; + } + else if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + } + + /* Get an account for the ACME server for this MD */ + if (md->ca_account && !acct_installed) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "re-use account '%s'", md->ca_account); + rv = md_acme_use_acct(ad->acme, d->store, d->p, md->ca_account); + if (APR_STATUS_IS_ENOENT(rv) || APR_STATUS_IS_EINVAL(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "rejected %s", md->ca_account); + md->ca_account = NULL; + update = 1; + rv = APR_SUCCESS; + } + } + + if (APR_SUCCESS == rv && !md->ca_account) { + /* Find a local account for server, store at MD */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: looking at existing accounts", + d->proto->protocol); + if (APR_SUCCESS == md_acme_find_acct(ad->acme, d->store, d->p)) { + md->ca_account = md_acme_get_acct_id(ad->acme); + update = 1; + } + } + + if (APR_SUCCESS == rv && !md->ca_account) { + /* 2.2 No local account exists, create a new one */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: creating new account", + d->proto->protocol); + + if (!ad->md->contacts || apr_is_empty_array(md->contacts)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, + "no contact information for md %s", md->name); + rv = APR_EINVAL; + goto out; + } + + if (APR_SUCCESS == (rv = md_acme_create_acct(ad->acme, d->p, md->contacts, + md->ca_agreement)) + && APR_SUCCESS == (rv = md_acme_acct_save_staged(ad->acme, d->store, md, d->p))) { + md->ca_account = MD_ACME_ACCT_STAGED; + update = 1; + } + } + +out: + if (APR_SUCCESS == rv) { + const char *agreement = md_acme_get_agreement(ad->acme); + /* Persist the account chosen at the md so we use the same on future runs */ + if (agreement && !md->ca_agreement) { + md->ca_agreement = agreement; + update = 1; + } + if (update) { + rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); + } + } + return rv; +} + +/**************************************************************************************************/ +/* authz/challenge setup */ + +/** + * Pre-Req: we have an account for the ACME server that has accepted the current license agreement + * For each domain in MD: + * - check if there already is a valid AUTHZ resource + * - if ot, create an AUTHZ resource with challenge data + */ +static apr_status_t ad_setup_authz(md_proto_driver_t *d) +{ + md_acme_driver_t *ad = d->baton; + apr_status_t rv; + md_t *md = ad->md; + md_acme_authz_t *authz; + int i; + int changed = 0; + + assert(ad->md); + assert(ad->acme); + + ad->phase = "check authz"; + + /* For each domain in MD: AUTHZ setup + * if an AUTHZ resource is known, check if it is still valid + * if known AUTHZ resource is not valid, remove, goto 4.1.1 + * if no AUTHZ available, create a new one for the domain, store it + */ + rv = md_acme_authz_set_load(d->store, MD_SG_STAGING, md->name, &ad->authz_set, d->p); + if (!ad->authz_set || APR_STATUS_IS_ENOENT(rv)) { + ad->authz_set = md_acme_authz_set_create(d->p); + rv = APR_SUCCESS; + } + else if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: loading authz data", md->name); + md_acme_authz_set_purge(d->store, MD_SG_STAGING, d->p, md->name); + return APR_EAGAIN; + } + + /* Remove anything we no longer need */ + for (i = 0; i < ad->authz_set->authzs->nelts;) { + authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*); + if (!md_contains(md, authz->domain, 0)) { + md_acme_authz_set_remove(ad->authz_set, authz->domain); + changed = 1; + } + else { + ++i; + } + } + + /* Add anything we do not already have */ + for (i = 0; i < md->domains->nelts && APR_SUCCESS == rv; ++i) { + const char *domain = APR_ARRAY_IDX(md->domains, i, const char *); + authz = md_acme_authz_set_get(ad->authz_set, domain); + if (authz) { + /* check valid */ + rv = md_acme_authz_update(authz, ad->acme, d->store, d->p); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: updated authz for %s", + md->name, domain); + if (APR_SUCCESS != rv) { + md_acme_authz_set_remove(ad->authz_set, domain); + authz = NULL; + changed = 1; + } + } + if (!authz) { + /* create new one */ + rv = md_acme_authz_register(&authz, ad->acme, d->store, domain, d->p); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: created authz for %s", + md->name, domain); + if (APR_SUCCESS == rv) { + rv = md_acme_authz_set_add(ad->authz_set, authz); + changed = 1; + } + } + } + + /* Save any changes */ + if (APR_SUCCESS == rv && changed) { + rv = md_acme_authz_set_save(d->store, d->p, MD_SG_STAGING, md->name, ad->authz_set, 0); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, "%s: saved", md->name); + } + + return rv; +} + +/** + * Pre-Req: all domains have a AUTHZ resources at the ACME server + * For each domain in MD: + * - if AUTHZ resource is 'valid' -> continue + * - if AUTHZ resource is 'pending': + * - find preferred challenge choice + * - calculate challenge data for httpd to find + * - POST challenge start to ACME server + * For each domain in MD where AUTHZ is 'pending', until overall timeout: + * - wait a certain time, check status again + * If not all AUTHZ are valid, fail + */ +static apr_status_t ad_start_challenges(md_proto_driver_t *d) +{ + md_acme_driver_t *ad = d->baton; + apr_status_t rv = APR_SUCCESS; + md_acme_authz_t *authz; + int i, changed = 0; + + assert(ad->md); + assert(ad->acme); + assert(ad->authz_set); + + ad->phase = "start challenges"; + + for (i = 0; i < ad->authz_set->authzs->nelts && APR_SUCCESS == rv; ++i) { + authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: check AUTHZ for %s", + ad->md->name, authz->domain); + if (APR_SUCCESS != (rv = md_acme_authz_update(authz, ad->acme, d->store, d->p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: check authz for %s", + ad->md->name, authz->domain); + break; + } + + switch (authz->state) { + case MD_ACME_AUTHZ_S_VALID: + break; + + case MD_ACME_AUTHZ_S_PENDING: + rv = md_acme_authz_respond(authz, ad->acme, d->store, ad->ca_challenges, + d->md->pkey_spec, d->p); + changed = 1; + break; + + default: + rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "%s: unexpected AUTHZ state %d at %s", + authz->domain, authz->state, authz->location); + break; + } + } + + if (APR_SUCCESS == rv && changed) { + rv = md_acme_authz_set_save(d->store, d->p, MD_SG_STAGING, ad->md->name, ad->authz_set, 0); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, "%s: saved", ad->md->name); + } + return rv; +} + +static apr_status_t check_challenges(void *baton, int attempt) +{ + md_proto_driver_t *d = baton; + md_acme_driver_t *ad = d->baton; + md_acme_authz_t *authz; + apr_status_t rv = APR_SUCCESS; + int i; + + for (i = 0; i < ad->authz_set->authzs->nelts && APR_SUCCESS == rv; ++i) { + authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: check AUTHZ for %s(%d. attempt)", + ad->md->name, authz->domain, attempt); + if (APR_SUCCESS == (rv = md_acme_authz_update(authz, ad->acme, d->store, d->p))) { + switch (authz->state) { + case MD_ACME_AUTHZ_S_VALID: + break; + case MD_ACME_AUTHZ_S_PENDING: + rv = APR_EAGAIN; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "%s: status pending at %s", authz->domain, authz->location); + break; + default: + rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "%s: unexpected AUTHZ state %d at %s", + authz->domain, authz->state, authz->location); + break; + } + } + } + return rv; +} + +static apr_status_t ad_monitor_challenges(md_proto_driver_t *d) +{ + md_acme_driver_t *ad = d->baton; + apr_status_t rv; + + assert(ad->md); + assert(ad->acme); + assert(ad->authz_set); + + ad->phase = "monitor challenges"; + rv = md_util_try(check_challenges, d, 0, ad->authz_monitor_timeout, 0, 0, 1); + + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, d->p, + "%s: checked all domain authorizations", ad->md->name); + return rv; +} + +/**************************************************************************************************/ +/* poll cert */ + +static void get_up_link(md_proto_driver_t *d, apr_table_t *headers) +{ + md_acme_driver_t *ad = d->baton; + + ad->next_up_link = md_link_find_relation(headers, d->p, "up"); + if (ad->next_up_link) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, + "server reports up link as %s", ad->next_up_link); + } +} + +static apr_status_t read_http_cert(md_cert_t **pcert, apr_pool_t *p, + const md_http_response_t *res) +{ + apr_status_t rv = APR_SUCCESS; + + if (APR_SUCCESS != (rv = md_cert_read_http(pcert, p, res)) + && APR_STATUS_IS_ENOENT(rv)) { + rv = APR_EAGAIN; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "cert not in response from %s", res->req->url); + } + return rv; +} + +static apr_status_t on_got_cert(md_acme_t *acme, const md_http_response_t *res, void *baton) +{ + md_proto_driver_t *d = baton; + md_acme_driver_t *ad = d->baton; + apr_status_t rv = APR_SUCCESS; + + (void)acme; + if (APR_SUCCESS == (rv = read_http_cert(&ad->cert, d->p, res))) { + rv = md_store_save(d->store, d->p, MD_SG_STAGING, ad->md->name, MD_FN_CERT, + MD_SV_CERT, ad->cert, 0); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "cert parsed and saved"); + if (APR_SUCCESS == rv) { + get_up_link(d, res->headers); + } + } + return rv; +} + +static apr_status_t get_cert(void *baton, int attempt) +{ + md_proto_driver_t *d = baton; + md_acme_driver_t *ad = d->baton; + + (void)attempt; + return md_acme_GET(ad->acme, ad->md->cert_url, NULL, NULL, on_got_cert, d); +} + +static apr_status_t ad_cert_poll(md_proto_driver_t *d, int only_once) +{ + md_acme_driver_t *ad = d->baton; + apr_status_t rv; + + assert(ad->md); + assert(ad->acme); + assert(ad->md->cert_url); + + ad->phase = "poll certificate"; + if (only_once) { + rv = get_cert(d, 0); + } + else { + rv = md_util_try(get_cert, d, 1, ad->cert_poll_timeout, 0, 0, 1); + } + + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "poll for cert at %s", ad->md->cert_url); + return rv; +} + +/**************************************************************************************************/ +/* cert setup */ + +static apr_status_t on_init_csr_req(md_acme_req_t *req, void *baton) +{ + md_proto_driver_t *d = baton; + md_acme_driver_t *ad = d->baton; + md_json_t *jpayload; + + jpayload = md_json_create(req->p); + md_json_sets("new-cert", jpayload, MD_KEY_RESOURCE, NULL); + md_json_sets(ad->csr_der_64, jpayload, MD_KEY_CSR, NULL); + + return md_acme_req_body_init(req, jpayload); +} + +static apr_status_t csr_req(md_acme_t *acme, const md_http_response_t *res, void *baton) +{ + md_proto_driver_t *d = baton; + md_acme_driver_t *ad = d->baton; + apr_status_t rv = APR_SUCCESS; + + (void)acme; + ad->md->cert_url = apr_table_get(res->headers, "location"); + if (!ad->md->cert_url) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, + "cert created without giving its location header"); + return APR_EINVAL; + } + if (APR_SUCCESS != (rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, + "%s: saving cert url %s", ad->md->name, ad->md->cert_url); + return rv; + } + + /* Check if it already was sent with this response */ + ad->next_up_link = NULL; + if (APR_SUCCESS == (rv = md_cert_read_http(&ad->cert, d->p, res))) { + rv = md_cert_save(d->store, d->p, MD_SG_STAGING, ad->md->name, ad->cert, 0); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "cert parsed and saved"); + if (APR_SUCCESS == rv) { + get_up_link(d, res->headers); + } + } + else if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "cert not in response, need to poll %s", ad->md->cert_url); + } + + return rv; +} + +/** + * Pre-Req: all domains have been validated by the ACME server, e.g. all have AUTHZ + * resources that have status 'valid' + * - Setup private key, if not already there + * - Generate a CSR with org, contact, etc + * - Optionally enable must-staple OCSP extension + * - Submit CSR, expect 201 with location + * - POLL location for certificate + * - store certificate + * - retrieve cert chain information from cert + * - GET cert chain + * - store cert chain + */ +static apr_status_t ad_setup_certificate(md_proto_driver_t *d) +{ + md_acme_driver_t *ad = d->baton; + md_pkey_t *privkey; + apr_status_t rv; + + ad->phase = "setup cert privkey"; + + rv = md_pkey_load(d->store, MD_SG_STAGING, ad->md->name, &privkey, d->p); + if (APR_STATUS_IS_ENOENT(rv)) { + if (APR_SUCCESS == (rv = md_pkey_gen(&privkey, d->p, d->md->pkey_spec))) { + rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, ad->md->name, privkey, 1); + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: generate privkey", ad->md->name); + } + + if (APR_SUCCESS == rv) { + ad->phase = "setup csr"; + rv = md_cert_req_create(&ad->csr_der_64, ad->md, privkey, d->p); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: create CSR", ad->md->name); + } + + if (APR_SUCCESS == rv) { + ad->phase = "submit csr"; + rv = md_acme_POST(ad->acme, ad->acme->new_cert, on_init_csr_req, NULL, csr_req, d); + } + + if (APR_SUCCESS == rv) { + if (!ad->cert) { + rv = ad_cert_poll(d, 0); + } + } + return rv; +} + +/**************************************************************************************************/ +/* cert chain retrieval */ + +static apr_status_t on_add_chain(md_acme_t *acme, const md_http_response_t *res, void *baton) +{ + md_proto_driver_t *d = baton; + md_acme_driver_t *ad = d->baton; + apr_status_t rv = APR_SUCCESS; + md_cert_t *cert; + const char *ct; + + (void)acme; + ct = apr_table_get(res->headers, "Content-Type"); + if (ct && !strcmp("application/x-pkcs7-mime", ct)) { + /* root cert most likely, end it here */ + return APR_SUCCESS; + } + + if (APR_SUCCESS == (rv = read_http_cert(&cert, d->p, res))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain cert parsed"); + APR_ARRAY_PUSH(ad->chain, md_cert_t *) = cert; + if (APR_SUCCESS == rv) { + get_up_link(d, res->headers); + } + } + return rv; +} + +static apr_status_t get_chain(void *baton, int attempt) +{ + md_proto_driver_t *d = baton; + md_acme_driver_t *ad = d->baton; + const char *prev_link = NULL; + apr_status_t rv = APR_SUCCESS; + + while (APR_SUCCESS == rv && ad->chain->nelts < 10) { + int nelts = ad->chain->nelts; + + if (ad->next_up_link && (!prev_link || strcmp(prev_link, ad->next_up_link))) { + prev_link = ad->next_up_link; + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "next issuer is %s", ad->next_up_link); + rv = md_acme_GET(ad->acme, ad->next_up_link, NULL, NULL, on_add_chain, d); + + if (APR_SUCCESS == rv && nelts == ad->chain->nelts) { + break; + } + } + else { + rv = APR_SUCCESS; + break; + } + } + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "got chain with %d certs (%d. attempt)", ad->chain->nelts, attempt); + return rv; +} + +static apr_status_t ad_chain_install(md_proto_driver_t *d) +{ + md_acme_driver_t *ad = d->baton; + apr_status_t rv; + + /* We should have that from initial cert retrieval, but if we restarted + * or switched child process, we need to retrieve this again from the + * certificate resources. */ + if (!ad->next_up_link) { + if (APR_SUCCESS != (rv = ad_cert_poll(d, 0))) { + return rv; + } + if (!ad->next_up_link) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "server reports no link header 'up' for certificate at %s", ad->md->cert_url); + return APR_EINVAL; + } + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, + "chain starts at %s", ad->next_up_link); + + ad->chain = apr_array_make(d->p, 5, sizeof(md_cert_t *)); + if (APR_SUCCESS == (rv = md_util_try(get_chain, d, 0, ad->cert_poll_timeout, 0, 0, 0))) { + rv = md_store_save(d->store, d->p, MD_SG_STAGING, ad->md->name, MD_FN_CHAIN, + MD_SV_CHAIN, ad->chain, 0); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain fetched and saved"); + } + return rv; +} + +/**************************************************************************************************/ +/* ACME driver init */ + +static apr_status_t acme_driver_init(md_proto_driver_t *d) +{ + md_acme_driver_t *ad; + apr_status_t rv = APR_SUCCESS; + + ad = apr_pcalloc(d->p, sizeof(*ad)); + + d->baton = ad; + ad->driver = d; + + ad->authz_monitor_timeout = apr_time_from_sec(30); + ad->cert_poll_timeout = apr_time_from_sec(30); + + /* We can only support challenges if the server is reachable from the outside + * via port 80 and/or 443. These ports might be mapped for httpd to something + * else, but a mapping needs to exist. */ + ad->ca_challenges = apr_array_make(d->p, 3, sizeof(const char *)); + if (d->challenge) { + /* we have been told to use this type */ + APR_ARRAY_PUSH(ad->ca_challenges, const char*) = apr_pstrdup(d->p, d->challenge); + } + else if (d->md->ca_challenges && d->md->ca_challenges->nelts > 0) { + /* pre-configured set for this managed domain */ + apr_array_cat(ad->ca_challenges, d->md->ca_challenges); + } + else { + /* free to chose. Add all we support and see what we get offered */ + APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_HTTP01; + APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_TLSSNI01; + } + + if (!d->can_http && !d->can_https) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, d->p, "%s: the server seems neither " + "reachable via http (port 80) nor https (port 443). The ACME protocol " + "needs at least one of those so the CA can talk to the server and verify " + "a domain ownership.", d->md->name); + return APR_EGENERAL; + } + + if (!d->can_http) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_HTTP01, 0); + } + if (!d->can_https) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_TLSSNI01, 0); + } + + if (apr_is_empty_array(ad->ca_challenges)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, d->p, "%s: specific CA challenge methods " + "have been configured, but the server is unable to use any of those. " + "For 'http-01' it needs to be reachable on port 80, for 'tls-sni-01'" + " port 443 is needed.", d->md->name); + return APR_EGENERAL; + } + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, d->p, "%s: init driver", d->md->name); + + return rv; +} + +/**************************************************************************************************/ +/* ACME staging */ + +static apr_status_t acme_stage(md_proto_driver_t *d) +{ + md_acme_driver_t *ad = d->baton; + int reset_staging = d->reset; + apr_status_t rv = APR_SUCCESS; + int renew = 1; + + if (md_log_is_level(d->p, MD_LOG_DEBUG)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: staging started, " + "state=%d, can_http=%d, can_https=%d, challenges='%s'", + d->md->name, d->md->state, d->can_http, d->can_https, + apr_array_pstrcat(d->p, ad->ca_challenges, ' ')); + } + + if (!reset_staging) { + rv = md_load(d->store, MD_SG_STAGING, d->md->name, &ad->md, d->p); + if (APR_SUCCESS == rv) { + /* So, we have a copy in staging, but is it a recent or an old one? */ + if (md_is_newer(d->store, MD_SG_DOMAINS, MD_SG_STAGING, d->md->name, d->p)) { + reset_staging = 1; + } + } + else if (APR_STATUS_IS_ENOENT(rv)) { + reset_staging = 1; + rv = APR_SUCCESS; + } + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "%s: checked staging area, will%s reset", + d->md->name, reset_staging? "" : " not"); + } + + if (reset_staging) { + /* reset the staging area for this domain */ + rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); + if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) { + return rv; + } + rv = APR_SUCCESS; + ad->md = NULL; + } + + if (ad->md && ad->md->state == MD_S_MISSING) { + /* There is config information missing. It makes no sense to drive this MD further */ + rv = APR_INCOMPLETE; + goto out; + } + + if (ad->md) { + /* staging in progress. look for new ACME account information collected there */ + rv = md_reg_creds_get(&ad->ncreds, d->reg, MD_SG_STAGING, d->md, d->p); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: checked creds", d->md->name); + if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + } + } + + /* Find out where we're at with this managed domain */ + if (ad->ncreds && ad->ncreds->privkey && ad->ncreds->pubcert) { + /* There is a full set staged, to be loaded */ + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: all data staged", d->md->name); + renew = 0; + } + + if (renew) { + if (APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, d->md->ca_url, d->proxy_url)) + || APR_SUCCESS != (rv = md_acme_setup(ad->acme))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, "%s: setup ACME(%s)", + d->md->name, d->md->ca_url); + return rv; + } + + if (!ad->md) { + /* re-initialize staging */ + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: setup staging", d->md->name); + md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); + ad->md = md_copy(d->p, d->md); + ad->md->cert_url = NULL; /* do not retrieve the old cert */ + rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: save staged md", + ad->md->name); + } + + if (APR_SUCCESS == rv && !ad->cert) { + md_cert_load(d->store, MD_SG_STAGING, ad->md->name, &ad->cert, d->p); + } + + if (APR_SUCCESS == rv && !ad->cert) { + ad->phase = "get certificate"; + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: need certificate", d->md->name); + + /* Chose (or create) and ACME account to use */ + rv = ad_set_acct(d); + + /* Check that the account agreed to the terms-of-service, otherwise + * requests for new authorizations are denied. ToS may change during the + * lifetime of an account */ + if (APR_SUCCESS == rv) { + const char *required; + + ad->phase = "check agreement"; + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, + "%s: check Terms-of-Service agreement", d->md->name); + + rv = md_acme_check_agreement(ad->acme, d->p, ad->md->ca_agreement, &required); + + if (APR_STATUS_IS_INCOMPLETE(rv) && required) { + /* The CA wants the user to agree to Terms-of-Services. Until the user + * has reconfigured and restarted the server, this MD cannot be + * driven further */ + ad->md->state = MD_S_MISSING; + md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); + + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "%s: the CA requires you to accept the terms-of-service " + "as specified in <%s>. " + "Please read the document that you find at that URL and, " + "if you agree to the conditions, configure " + "\"MDCertificateAgreement url\" " + "with exactly that URL in your Apache. " + "Then (graceful) restart the server to activate.", + ad->md->name, required); + goto out; + } + } + + /* If we know a cert's location, try to get it. Previous download might + * have failed. If server 404 it, we clear our memory of it. */ + if (APR_SUCCESS == rv && ad->md->cert_url) { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, + "%s: polling certificate", d->md->name); + rv = ad_cert_poll(d, 1); + if (APR_STATUS_IS_ENOENT(rv)) { + /* Server reports to know nothing about it. */ + ad->md->cert_url = NULL; + rv = md_reg_update(d->reg, d->p, ad->md->name, ad->md, MD_UPD_CERT_URL); + } + } + + if (APR_SUCCESS == rv && !ad->cert) { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, + "%s: setup new authorization", d->md->name); + if (APR_SUCCESS != (rv = ad_setup_authz(d))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: setup authz resource", + ad->md->name); + goto out; + } + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, + "%s: setup new challenges", d->md->name); + if (APR_SUCCESS != (rv = ad_start_challenges(d))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: start challenges", + ad->md->name); + goto out; + } + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, + "%s: monitoring challenge status", d->md->name); + if (APR_SUCCESS != (rv = ad_monitor_challenges(d))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: monitor challenges", + ad->md->name); + goto out; + } + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, + "%s: creating certificate request", d->md->name); + if (APR_SUCCESS != (rv = ad_setup_certificate(d))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: setup certificate", + ad->md->name); + goto out; + } + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, + "%s: received certificate", d->md->name); + } + + } + + if (APR_SUCCESS == rv && !ad->chain) { + /* have we created this already? */ + md_chain_load(d->store, MD_SG_STAGING, ad->md->name, &ad->chain, d->p); + } + if (APR_SUCCESS == rv && !ad->chain) { + ad->phase = "install chain"; + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, + "%s: retrieving certificate chain", d->md->name); + rv = ad_chain_install(d); + } + + if (APR_SUCCESS == rv && !ad->pubcert) { + /* have we created this already? */ + md_pubcert_load(d->store, MD_SG_STAGING, ad->md->name, &ad->pubcert, d->p); + } + if (APR_SUCCESS == rv && !ad->pubcert) { + /* combine cert + chain into the pubcert */ + ad->pubcert = apr_array_make(d->p, ad->chain->nelts + 1, sizeof(md_cert_t*)); + APR_ARRAY_PUSH(ad->pubcert, md_cert_t *) = ad->cert; + apr_array_cat(ad->pubcert, ad->chain); + rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, ad->md->name, ad->pubcert, 0); + } + + if (APR_SUCCESS == rv && ad->cert) { + apr_time_t now = apr_time_now(); + apr_interval_time_t max_delay, delay_activation; + + /* determine when this cert should be activated */ + d->stage_valid_from = md_cert_get_not_before(ad->cert); + if (d->md->state == MD_S_COMPLETE && d->md->expires > now) { + /** + * The MD is complete and un-expired. This is a renewal run. + * Give activation 24 hours leeway (if we have that time) to + * accommodate for clients with somewhat weird clocks. + */ + delay_activation = apr_time_from_sec(MD_SECS_PER_DAY); + if (delay_activation > (max_delay = d->md->expires - now)) { + delay_activation = max_delay; + } + d->stage_valid_from += delay_activation; + } + } + } +out: + return rv; +} + +static apr_status_t acme_driver_stage(md_proto_driver_t *d) +{ + md_acme_driver_t *ad = d->baton; + apr_status_t rv; + + ad->phase = "ACME staging"; + if (APR_SUCCESS == (rv = acme_stage(d))) { + ad->phase = "staging done"; + } + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: %s, %s", + d->md->name, d->proto->protocol, ad->phase); + return rv; +} + +/**************************************************************************************************/ +/* ACME preload */ + +static apr_status_t acme_preload(md_store_t *store, md_store_group_t load_group, + const char *name, const char *proxy_url, apr_pool_t *p) +{ + apr_status_t rv; + md_pkey_t *privkey, *acct_key; + md_t *md; + apr_array_header_t *pubcert; + struct md_acme_acct_t *acct; + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "%s: preload start", name); + /* Load all data which will be taken into the DOMAIN storage group. + * This serves several purposes: + * 1. It's a format check on the input data. + * 2. We write back what we read, creating data with our own access permissions + * 3. We ignore any other accumulated data in STAGING + * 4. Once TMP is verified, we can swap/archive groups with a rename + * 5. Reading/Writing the data will apply/remove any group specific data encryption. + * With the exemption that DOMAINS and TMP must apply the same policy/keys. + */ + if (APR_SUCCESS != (rv = md_load(store, MD_SG_STAGING, name, &md, p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading md json", name); + return rv; + } + if (APR_SUCCESS != (rv = md_pkey_load(store, MD_SG_STAGING, name, &privkey, p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading staging private key", name); + return rv; + } + if (APR_SUCCESS != (rv = md_pubcert_load(store, MD_SG_STAGING, name, &pubcert, p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading pubcert", name); + return rv; + } + + /* See if staging holds a new or modified account data */ + rv = md_acme_acct_load(&acct, &acct_key, store, MD_SG_STAGING, name, p); + if (APR_STATUS_IS_ENOENT(rv)) { + acct = NULL; + acct_key = NULL; + rv = APR_SUCCESS; + } + else if (APR_SUCCESS != rv) { + return rv; + } + + /* Remove any authz information we have here or in MD_SG_CHALLENGES */ + md_acme_authz_set_purge(store, MD_SG_STAGING, p, name); + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "%s: staged data load, purging tmp space", name); + rv = md_store_purge(store, p, load_group, name); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: error purging preload storage", name); + return rv; + } + + if (acct) { + md_acme_t *acme; + + if (APR_SUCCESS != (rv = md_acme_create(&acme, p, md->ca_url, proxy_url)) + || APR_SUCCESS != (rv = md_acme_acct_save(store, p, acme, acct, acct_key))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: error saving acct", name); + return rv; + } + md->ca_account = acct->id; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: saved ACME account %s", + name, acct->id); + } + + if (APR_SUCCESS != (rv = md_save(store, p, load_group, md, 1))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving md json", name); + return rv; + } + if (APR_SUCCESS != (rv = md_pubcert_save(store, p, load_group, name, pubcert, 1))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving cert chain", name); + return rv; + } + if (APR_SUCCESS != (rv = md_pkey_save(store, p, load_group, name, privkey, 1))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving private key", name); + return rv; + } + + return rv; +} + +static apr_status_t acme_driver_preload(md_proto_driver_t *d, md_store_group_t group) +{ + md_acme_driver_t *ad = d->baton; + apr_status_t rv; + + ad->phase = "ACME preload"; + if (APR_SUCCESS == (rv = acme_preload(d->store, group, d->md->name, d->proxy_url, d->p))) { + ad->phase = "preload done"; + } + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: %s, %s", + d->md->name, d->proto->protocol, ad->phase); + return rv; +} + +static md_proto_t ACME_PROTO = { + MD_PROTO_ACME, acme_driver_init, acme_driver_stage, acme_driver_preload +}; + +apr_status_t md_acme_protos_add(apr_hash_t *protos, apr_pool_t *p) +{ + (void)p; + apr_hash_set(protos, MD_PROTO_ACME, sizeof(MD_PROTO_ACME)-1, &ACME_PROTO); + return APR_SUCCESS; +} diff --git a/modules/md/md_core.c b/modules/md/md_core.c new file mode 100644 index 0000000..51ad005 --- /dev/null +++ b/modules/md/md_core.c @@ -0,0 +1,428 @@ +/* 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. + */ + +#include <assert.h> +#include <stdlib.h> + +#include <apr_lib.h> +#include <apr_strings.h> +#include <apr_tables.h> +#include <apr_time.h> +#include <apr_date.h> + +#include "md_json.h" +#include "md.h" +#include "md_crypt.h" +#include "md_log.h" +#include "md_store.h" +#include "md_util.h" + + +int md_contains(const md_t *md, const char *domain, int case_sensitive) +{ + return md_array_str_index(md->domains, domain, 0, case_sensitive) >= 0; +} + +const char *md_common_name(const md_t *md1, const md_t *md2) +{ + int i; + + if (md1 == NULL || md1->domains == NULL + || md2 == NULL || md2->domains == NULL) { + return NULL; + } + + for (i = 0; i < md1->domains->nelts; ++i) { + const char *name1 = APR_ARRAY_IDX(md1->domains, i, const char*); + if (md_contains(md2, name1, 0)) { + return name1; + } + } + return NULL; +} + +int md_domains_overlap(const md_t *md1, const md_t *md2) +{ + return md_common_name(md1, md2) != NULL; +} + +apr_size_t md_common_name_count(const md_t *md1, const md_t *md2) +{ + int i; + apr_size_t hits; + + if (md1 == NULL || md1->domains == NULL + || md2 == NULL || md2->domains == NULL) { + return 0; + } + + hits = 0; + for (i = 0; i < md1->domains->nelts; ++i) { + const char *name1 = APR_ARRAY_IDX(md1->domains, i, const char*); + if (md_contains(md2, name1, 0)) { + ++hits; + } + } + return hits; +} + +md_t *md_create_empty(apr_pool_t *p) +{ + md_t *md = apr_pcalloc(p, sizeof(*md)); + if (md) { + md->domains = apr_array_make(p, 5, sizeof(const char *)); + md->contacts = apr_array_make(p, 5, sizeof(const char *)); + md->drive_mode = MD_DRIVE_DEFAULT; + md->require_https = MD_REQUIRE_UNSET; + md->must_staple = -1; + md->transitive = -1; + md->defn_name = "unknown"; + md->defn_line_number = 0; + } + return md; +} + +int md_equal_domains(const md_t *md1, const md_t *md2, int case_sensitive) +{ + int i; + if (md1->domains->nelts == md2->domains->nelts) { + for (i = 0; i < md1->domains->nelts; ++i) { + const char *name1 = APR_ARRAY_IDX(md1->domains, i, const char*); + if (!md_contains(md2, name1, case_sensitive)) { + return 0; + } + } + return 1; + } + return 0; +} + +int md_contains_domains(const md_t *md1, const md_t *md2) +{ + int i; + if (md1->domains->nelts >= md2->domains->nelts) { + for (i = 0; i < md2->domains->nelts; ++i) { + const char *name2 = APR_ARRAY_IDX(md2->domains, i, const char*); + if (!md_contains(md1, name2, 0)) { + return 0; + } + } + return 1; + } + return 0; +} + +md_t *md_find_closest_match(apr_array_header_t *mds, const md_t *md) +{ + md_t *candidate, *m; + apr_size_t cand_n, n; + int i; + + candidate = md_get_by_name(mds, md->name); + if (!candidate) { + /* try to find an instance that contains all domain names from md */ + for (i = 0; i < mds->nelts; ++i) { + m = APR_ARRAY_IDX(mds, i, md_t *); + if (md_contains_domains(m, md)) { + return m; + } + } + /* no matching name and no md in the list has all domains. + * We consider that managed domain as closest match that contains at least one + * domain name from md, ONLY if there is no other one that also has. + */ + cand_n = 0; + for (i = 0; i < mds->nelts; ++i) { + m = APR_ARRAY_IDX(mds, i, md_t *); + n = md_common_name_count(md, m); + if (n > cand_n) { + candidate = m; + cand_n = n; + } + } + } + return candidate; +} + +md_t *md_get_by_name(struct apr_array_header_t *mds, const char *name) +{ + int i; + for (i = 0; i < mds->nelts; ++i) { + md_t *md = APR_ARRAY_IDX(mds, i, md_t *); + if (!strcmp(name, md->name)) { + return md; + } + } + return NULL; +} + +md_t *md_get_by_domain(struct apr_array_header_t *mds, const char *domain) +{ + int i; + for (i = 0; i < mds->nelts; ++i) { + md_t *md = APR_ARRAY_IDX(mds, i, md_t *); + if (md_contains(md, domain, 0)) { + return md; + } + } + return NULL; +} + +md_t *md_get_by_dns_overlap(struct apr_array_header_t *mds, const md_t *md) +{ + int i; + for (i = 0; i < mds->nelts; ++i) { + md_t *o = APR_ARRAY_IDX(mds, i, md_t *); + if (strcmp(o->name, md->name) && md_common_name(o, md)) { + return o; + } + } + return NULL; +} + +md_t *md_create(apr_pool_t *p, apr_array_header_t *domains) +{ + md_t *md; + + md = md_create_empty(p); + md->domains = md_array_str_compact(p, domains, 0); + md->name = APR_ARRAY_IDX(md->domains, 0, const char *); + + return md; +} + +int md_should_renew(const md_t *md) +{ + apr_time_t now = apr_time_now(); + + if (md->expires <= now) { + return 1; + } + else if (md->expires > 0) { + double renew_win, life; + apr_interval_time_t left; + + renew_win = (double)md->renew_window; + if (md->renew_norm > 0 + && md->renew_norm > renew_win + && md->expires > md->valid_from) { + /* Calc renewal days as fraction of cert lifetime - if known */ + life = (double)(md->expires - md->valid_from); + renew_win = life * renew_win / (double)md->renew_norm; + } + + left = md->expires - now; + if (left <= renew_win) { + return 1; + } + } + return 0; +} + +/**************************************************************************************************/ +/* lifetime */ + +md_t *md_copy(apr_pool_t *p, const md_t *src) +{ + md_t *md; + + md = apr_pcalloc(p, sizeof(*md)); + if (md) { + memcpy(md, src, sizeof(*md)); + md->domains = apr_array_copy(p, src->domains); + md->contacts = apr_array_copy(p, src->contacts); + if (src->ca_challenges) { + md->ca_challenges = apr_array_copy(p, src->ca_challenges); + } + } + return md; +} + +md_t *md_clone(apr_pool_t *p, const md_t *src) +{ + md_t *md; + + md = apr_pcalloc(p, sizeof(*md)); + if (md) { + md->state = src->state; + md->name = apr_pstrdup(p, src->name); + md->require_https = src->require_https; + md->must_staple = src->must_staple; + md->drive_mode = src->drive_mode; + md->domains = md_array_str_compact(p, src->domains, 0); + md->pkey_spec = src->pkey_spec; + md->renew_norm = src->renew_norm; + md->renew_window = src->renew_window; + md->contacts = md_array_str_clone(p, src->contacts); + if (src->ca_url) md->ca_url = apr_pstrdup(p, src->ca_url); + if (src->ca_proto) md->ca_proto = apr_pstrdup(p, src->ca_proto); + if (src->ca_account) md->ca_account = apr_pstrdup(p, src->ca_account); + if (src->ca_agreement) md->ca_agreement = apr_pstrdup(p, src->ca_agreement); + if (src->defn_name) md->defn_name = apr_pstrdup(p, src->defn_name); + if (src->cert_url) md->cert_url = apr_pstrdup(p, src->cert_url); + md->defn_line_number = src->defn_line_number; + if (src->ca_challenges) { + md->ca_challenges = md_array_str_clone(p, src->ca_challenges); + } + } + return md; +} + +md_t *md_merge(apr_pool_t *p, const md_t *add, const md_t *base) +{ + md_t *n = apr_pcalloc(p, sizeof(*n)); + + n->ca_url = add->ca_url? add->ca_url : base->ca_url; + n->ca_proto = add->ca_proto? add->ca_proto : base->ca_proto; + n->ca_agreement = add->ca_agreement? add->ca_agreement : base->ca_agreement; + n->require_https = (add->require_https != MD_REQUIRE_UNSET)? add->require_https : base->require_https; + n->must_staple = (add->must_staple >= 0)? add->must_staple : base->must_staple; + n->drive_mode = (add->drive_mode != MD_DRIVE_DEFAULT)? add->drive_mode : base->drive_mode; + n->pkey_spec = add->pkey_spec? add->pkey_spec : base->pkey_spec; + n->renew_norm = (add->renew_norm > 0)? add->renew_norm : base->renew_norm; + n->renew_window = (add->renew_window > 0)? add->renew_window : base->renew_window; + n->transitive = (add->transitive >= 0)? add->transitive : base->transitive; + if (add->ca_challenges) { + n->ca_challenges = apr_array_copy(p, add->ca_challenges); + } + else if (base->ca_challenges) { + n->ca_challenges = apr_array_copy(p, base->ca_challenges); + } + return n; +} + +/**************************************************************************************************/ +/* format conversion */ + +md_json_t *md_to_json(const md_t *md, apr_pool_t *p) +{ + md_json_t *json = md_json_create(p); + if (json) { + apr_array_header_t *domains = md_array_str_compact(p, md->domains, 0); + md_json_sets(md->name, json, MD_KEY_NAME, NULL); + md_json_setsa(domains, json, MD_KEY_DOMAINS, NULL); + md_json_setsa(md->contacts, json, MD_KEY_CONTACTS, NULL); + md_json_setl(md->transitive, json, MD_KEY_TRANSITIVE, NULL); + md_json_sets(md->ca_account, json, MD_KEY_CA, MD_KEY_ACCOUNT, NULL); + md_json_sets(md->ca_proto, json, MD_KEY_CA, MD_KEY_PROTO, NULL); + md_json_sets(md->ca_url, json, MD_KEY_CA, MD_KEY_URL, NULL); + md_json_sets(md->ca_agreement, json, MD_KEY_CA, MD_KEY_AGREEMENT, NULL); + if (md->cert_url) { + md_json_sets(md->cert_url, json, MD_KEY_CERT, MD_KEY_URL, NULL); + } + if (md->pkey_spec) { + md_json_setj(md_pkey_spec_to_json(md->pkey_spec, p), json, MD_KEY_PKEY, NULL); + } + md_json_setl(md->state, json, MD_KEY_STATE, NULL); + md_json_setl(md->drive_mode, json, MD_KEY_DRIVE_MODE, NULL); + if (md->expires > 0) { + char *ts = apr_pcalloc(p, APR_RFC822_DATE_LEN); + apr_rfc822_date(ts, md->expires); + md_json_sets(ts, json, MD_KEY_CERT, MD_KEY_EXPIRES, NULL); + } + if (md->valid_from > 0) { + char *ts = apr_pcalloc(p, APR_RFC822_DATE_LEN); + apr_rfc822_date(ts, md->valid_from); + md_json_sets(ts, json, MD_KEY_CERT, MD_KEY_VALID_FROM, NULL); + } + if (md->renew_norm > 0) { + md_json_sets(apr_psprintf(p, "%ld%%", (long)(md->renew_window * 100L / md->renew_norm)), + json, MD_KEY_RENEW_WINDOW, NULL); + } + else { + md_json_setl((long)apr_time_sec(md->renew_window), json, MD_KEY_RENEW_WINDOW, NULL); + } + md_json_setb(md_should_renew(md), json, MD_KEY_RENEW, NULL); + if (md->ca_challenges && md->ca_challenges->nelts > 0) { + apr_array_header_t *na; + na = md_array_str_compact(p, md->ca_challenges, 0); + md_json_setsa(na, json, MD_KEY_CA, MD_KEY_CHALLENGES, NULL); + } + switch (md->require_https) { + case MD_REQUIRE_TEMPORARY: + md_json_sets(MD_KEY_TEMPORARY, json, MD_KEY_REQUIRE_HTTPS, NULL); + break; + case MD_REQUIRE_PERMANENT: + md_json_sets(MD_KEY_PERMANENT, json, MD_KEY_REQUIRE_HTTPS, NULL); + break; + default: + break; + } + md_json_setb(md->must_staple > 0, json, MD_KEY_MUST_STAPLE, NULL); + return json; + } + return NULL; +} + +md_t *md_from_json(md_json_t *json, apr_pool_t *p) +{ + const char *s; + md_t *md = md_create_empty(p); + if (md) { + md->name = md_json_dups(p, json, MD_KEY_NAME, NULL); + md_json_dupsa(md->domains, p, json, MD_KEY_DOMAINS, NULL); + md_json_dupsa(md->contacts, p, json, MD_KEY_CONTACTS, NULL); + md->ca_account = md_json_dups(p, json, MD_KEY_CA, MD_KEY_ACCOUNT, NULL); + md->ca_proto = md_json_dups(p, json, MD_KEY_CA, MD_KEY_PROTO, NULL); + md->ca_url = md_json_dups(p, json, MD_KEY_CA, MD_KEY_URL, NULL); + md->ca_agreement = md_json_dups(p, json, MD_KEY_CA, MD_KEY_AGREEMENT, NULL); + md->cert_url = md_json_dups(p, json, MD_KEY_CERT, MD_KEY_URL, NULL); + if (md_json_has_key(json, MD_KEY_PKEY, MD_KEY_TYPE, NULL)) { + md->pkey_spec = md_pkey_spec_from_json(md_json_getj(json, MD_KEY_PKEY, NULL), p); + } + md->state = (md_state_t)md_json_getl(json, MD_KEY_STATE, NULL); + md->drive_mode = (int)md_json_getl(json, MD_KEY_DRIVE_MODE, NULL); + md->domains = md_array_str_compact(p, md->domains, 0); + md->transitive = (int)md_json_getl(json, MD_KEY_TRANSITIVE, NULL); + s = md_json_dups(p, json, MD_KEY_CERT, MD_KEY_EXPIRES, NULL); + if (s && *s) { + md->expires = apr_date_parse_rfc(s); + } + s = md_json_dups(p, json, MD_KEY_CERT, MD_KEY_VALID_FROM, NULL); + if (s && *s) { + md->valid_from = apr_date_parse_rfc(s); + } + md->renew_norm = 0; + md->renew_window = apr_time_from_sec(md_json_getl(json, MD_KEY_RENEW_WINDOW, NULL)); + if (md->renew_window <= 0) { + s = md_json_gets(json, MD_KEY_RENEW_WINDOW, NULL); + if (s && strchr(s, '%')) { + int percent = atoi(s); + if (0 < percent && percent < 100) { + md->renew_norm = apr_time_from_sec(100 * MD_SECS_PER_DAY); + md->renew_window = apr_time_from_sec(percent * MD_SECS_PER_DAY); + } + } + } + if (md_json_has_key(json, MD_KEY_CA, MD_KEY_CHALLENGES, NULL)) { + md->ca_challenges = apr_array_make(p, 5, sizeof(const char*)); + md_json_dupsa(md->ca_challenges, p, json, MD_KEY_CA, MD_KEY_CHALLENGES, NULL); + } + md->require_https = MD_REQUIRE_OFF; + s = md_json_gets(json, MD_KEY_REQUIRE_HTTPS, NULL); + if (s && !strcmp(MD_KEY_TEMPORARY, s)) { + md->require_https = MD_REQUIRE_TEMPORARY; + } + else if (s && !strcmp(MD_KEY_PERMANENT, s)) { + md->require_https = MD_REQUIRE_PERMANENT; + } + md->must_staple = (int)md_json_getb(json, MD_KEY_MUST_STAPLE, NULL); + + return md; + } + return NULL; +} + diff --git a/modules/md/md_crypt.c b/modules/md/md_crypt.c new file mode 100644 index 0000000..e0aac3e --- /dev/null +++ b/modules/md/md_crypt.c @@ -0,0 +1,1323 @@ +/* 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. + */ + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> + +#include <apr_lib.h> +#include <apr_buckets.h> +#include <apr_file_io.h> +#include <apr_strings.h> + +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/rand.h> +#include <openssl/rsa.h> +#include <openssl/x509v3.h> + +#include "md.h" +#include "md_crypt.h" +#include "md_json.h" +#include "md_log.h" +#include "md_http.h" +#include "md_util.h" + +/* getpid for *NIX */ +#if APR_HAVE_SYS_TYPES_H +#include <sys/types.h> +#endif +#if APR_HAVE_UNISTD_H +#include <unistd.h> +#endif + +/* getpid for Windows */ +#if APR_HAVE_PROCESS_H +#include <process.h> +#endif + +#if defined(LIBRESSL_VERSION_NUMBER) +/* Missing from LibreSSL */ +#define MD_USE_OPENSSL_PRE_1_1_API (LIBRESSL_VERSION_NUMBER < 0x2070000f) +#else /* defined(LIBRESSL_VERSION_NUMBER) */ +#define MD_USE_OPENSSL_PRE_1_1_API (OPENSSL_VERSION_NUMBER < 0x10100000L) +#endif + +static int initialized; + +struct md_pkey_t { + apr_pool_t *pool; + EVP_PKEY *pkey; +}; + +#ifdef MD_HAVE_ARC4RANDOM + +static void seed_RAND(int pid) +{ + char seed[128]; + + (void)pid; + arc4random_buf(seed, sizeof(seed)); + RAND_seed(seed, sizeof(seed)); +} + +#else /* ifdef MD_HAVE_ARC4RANDOM */ + +static int rand_choosenum(int l, int h) +{ + int i; + char buf[50]; + + apr_snprintf(buf, sizeof(buf), "%.0f", + (((double)(rand()%RAND_MAX)/RAND_MAX)*(h-l))); + i = atoi(buf)+1; + if (i < l) i = l; + if (i > h) i = h; + return i; +} + +static void seed_RAND(int pid) +{ + unsigned char stackdata[256]; + /* stolen from mod_ssl/ssl_engine_rand.c */ + int n; + struct { + time_t t; + pid_t pid; + } my_seed; + + /* + * seed in the current time (usually just 4 bytes) + */ + my_seed.t = time(NULL); + + /* + * seed in the current process id (usually just 4 bytes) + */ + my_seed.pid = pid; + + RAND_seed((unsigned char *)&my_seed, sizeof(my_seed)); + + /* + * seed in some current state of the run-time stack (128 bytes) + */ + n = rand_choosenum(0, sizeof(stackdata)-128-1); + RAND_seed(stackdata+n, 128); +} + +#endif /*ifdef MD_HAVE_ARC4RANDOM (else part) */ + + +apr_status_t md_crypt_init(apr_pool_t *pool) +{ + (void)pool; + + if (!initialized) { + int pid = getpid(); + + ERR_load_crypto_strings(); + OpenSSL_add_all_algorithms(); + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, pool, "initializing RAND"); + while (!RAND_status()) { + seed_RAND(pid); + } + + initialized = 1; + } + return APR_SUCCESS; +} + +typedef struct { + char *data; + apr_size_t len; +} buffer_rec; + +static apr_status_t fwrite_buffer(void *baton, apr_file_t *f, apr_pool_t *p) +{ + buffer_rec *buf = baton; + + (void)p; + return apr_file_write_full(f, buf->data, buf->len, &buf->len); +} + +apr_status_t md_rand_bytes(unsigned char *buf, apr_size_t len, apr_pool_t *p) +{ + apr_status_t rv; + + if (len > INT_MAX) { + return APR_ENOTIMPL; + } + if (APR_SUCCESS == (rv = md_crypt_init(p))) { + RAND_bytes((unsigned char*)buf, (int)len); + } + return rv; +} + +typedef struct { + const char *pass_phrase; + int pass_len; +} passwd_ctx; + +static int pem_passwd(char *buf, int size, int rwflag, void *baton) +{ + passwd_ctx *ctx = baton; + + (void)rwflag; + if (ctx->pass_len > 0) { + if (ctx->pass_len < size) { + size = (int)ctx->pass_len; + } + memcpy(buf, ctx->pass_phrase, (size_t)size); + } + return ctx->pass_len; +} + +/**************************************************************************************************/ +/* date time things */ + +/* Get the apr time (micro seconds, since 1970) from an ASN1 time, as stored in X509 + * certificates. OpenSSL now has a utility function, but other *SSL derivatives have + * not caughts up yet or chose to ignore. An alternative is implemented, we prefer + * however the *SSL to maintain such things. + */ +static apr_time_t md_asn1_time_get(const ASN1_TIME* time) +{ +#if OPENSSL_VERSION_NUMBER < 0x10002000L || defined(LIBRESSL_VERSION_NUMBER) + /* courtesy: https://stackoverflow.com/questions/10975542/asn1-time-to-time-t-conversion#11263731 + * all bugs are mine */ + apr_time_exp_t t; + apr_time_t ts; + const char* str = (const char*) time->data; + apr_size_t i = 0; + + memset(&t, 0, sizeof(t)); + + if (time->type == V_ASN1_UTCTIME) {/* two digit year */ + t.tm_year = (str[i++] - '0') * 10; + t.tm_year += (str[i++] - '0'); + if (t.tm_year < 70) + t.tm_year += 100; + } + else if (time->type == V_ASN1_GENERALIZEDTIME) {/* four digit year */ + t.tm_year = (str[i++] - '0') * 1000; + t.tm_year+= (str[i++] - '0') * 100; + t.tm_year+= (str[i++] - '0') * 10; + t.tm_year+= (str[i++] - '0'); + t.tm_year -= 1900; + } + t.tm_mon = (str[i++] - '0') * 10; + t.tm_mon += (str[i++] - '0') - 1; /* -1 since January is 0 not 1. */ + t.tm_mday = (str[i++] - '0') * 10; + t.tm_mday+= (str[i++] - '0'); + t.tm_hour = (str[i++] - '0') * 10; + t.tm_hour+= (str[i++] - '0'); + t.tm_min = (str[i++] - '0') * 10; + t.tm_min += (str[i++] - '0'); + t.tm_sec = (str[i++] - '0') * 10; + t.tm_sec += (str[i++] - '0'); + + if (APR_SUCCESS == apr_time_exp_gmt_get(&ts, &t)) { + return ts; + } + return 0; +#else + int secs, days; + apr_time_t ts = apr_time_now(); + + if (ASN1_TIME_diff(&days, &secs, NULL, time)) { + ts += apr_time_from_sec((days * MD_SECS_PER_DAY) + secs); + } + return ts; +#endif +} + + +/**************************************************************************************************/ +/* private keys */ + +md_json_t *md_pkey_spec_to_json(const md_pkey_spec_t *spec, apr_pool_t *p) +{ + md_json_t *json = md_json_create(p); + if (json) { + switch (spec->type) { + case MD_PKEY_TYPE_DEFAULT: + md_json_sets("Default", json, MD_KEY_TYPE, NULL); + break; + case MD_PKEY_TYPE_RSA: + md_json_sets("RSA", json, MD_KEY_TYPE, NULL); + if (spec->params.rsa.bits >= MD_PKEY_RSA_BITS_MIN) { + md_json_setl((long)spec->params.rsa.bits, json, MD_KEY_BITS, NULL); + } + break; + default: + md_json_sets("Unsupported", json, MD_KEY_TYPE, NULL); + break; + } + } + return json; +} + +md_pkey_spec_t *md_pkey_spec_from_json(struct md_json_t *json, apr_pool_t *p) +{ + md_pkey_spec_t *spec = apr_pcalloc(p, sizeof(*spec)); + const char *s; + long l; + + if (spec) { + s = md_json_gets(json, MD_KEY_TYPE, NULL); + if (!s || !apr_strnatcasecmp("Default", s)) { + spec->type = MD_PKEY_TYPE_DEFAULT; + } + else if (!apr_strnatcasecmp("RSA", s)) { + spec->type = MD_PKEY_TYPE_RSA; + l = md_json_getl(json, MD_KEY_BITS, NULL); + if (l >= MD_PKEY_RSA_BITS_MIN) { + spec->params.rsa.bits = (unsigned int)l; + } + else { + spec->params.rsa.bits = MD_PKEY_RSA_BITS_DEF; + } + } + } + return spec; +} + +int md_pkey_spec_eq(md_pkey_spec_t *spec1, md_pkey_spec_t *spec2) +{ + if (spec1 == spec2) { + return 1; + } + if (spec1 && spec2 && spec1->type == spec2->type) { + switch (spec1->type) { + case MD_PKEY_TYPE_DEFAULT: + return 1; + case MD_PKEY_TYPE_RSA: + if (spec1->params.rsa.bits == spec2->params.rsa.bits) { + return 1; + } + break; + } + } + return 0; +} + +static md_pkey_t *make_pkey(apr_pool_t *p) +{ + md_pkey_t *pkey = apr_pcalloc(p, sizeof(*pkey)); + pkey->pool = p; + return pkey; +} + +static apr_status_t pkey_cleanup(void *data) +{ + md_pkey_t *pkey = data; + if (pkey->pkey) { + EVP_PKEY_free(pkey->pkey); + pkey->pkey = NULL; + } + return APR_SUCCESS; +} + +void md_pkey_free(md_pkey_t *pkey) +{ + pkey_cleanup(pkey); +} + +void *md_pkey_get_EVP_PKEY(struct md_pkey_t *pkey) +{ + return pkey->pkey; +} + +apr_status_t md_pkey_fload(md_pkey_t **ppkey, apr_pool_t *p, + const char *key, apr_size_t key_len, + const char *fname) +{ + apr_status_t rv = APR_ENOENT; + md_pkey_t *pkey; + BIO *bf; + passwd_ctx ctx; + + pkey = make_pkey(p); + if (NULL != (bf = BIO_new_file(fname, "r"))) { + ctx.pass_phrase = key; + ctx.pass_len = (int)key_len; + + ERR_clear_error(); + pkey->pkey = PEM_read_bio_PrivateKey(bf, NULL, pem_passwd, &ctx); + BIO_free(bf); + + if (pkey->pkey != NULL) { + rv = APR_SUCCESS; + apr_pool_cleanup_register(p, pkey, pkey_cleanup, apr_pool_cleanup_null); + } + else { + unsigned long err = ERR_get_error(); + rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, + "error loading pkey %s: %s (pass phrase was %snull)", fname, + ERR_error_string(err, NULL), key? "not " : ""); + } + } + *ppkey = (APR_SUCCESS == rv)? pkey : NULL; + return rv; +} + +static apr_status_t pkey_to_buffer(buffer_rec *buffer, md_pkey_t *pkey, apr_pool_t *p, + const char *pass, apr_size_t pass_len) +{ + BIO *bio = BIO_new(BIO_s_mem()); + const EVP_CIPHER *cipher = NULL; + pem_password_cb *cb = NULL; + void *cb_baton = NULL; + passwd_ctx ctx; + unsigned long err; + int i; + + if (!bio) { + return APR_ENOMEM; + } + if (pass_len > INT_MAX) { + return APR_EINVAL; + } + if (pass && pass_len > 0) { + ctx.pass_phrase = pass; + ctx.pass_len = (int)pass_len; + cb = pem_passwd; + cb_baton = &ctx; + cipher = EVP_aes_256_cbc(); + if (!cipher) { + return APR_ENOTIMPL; + } + } + + ERR_clear_error(); + if (!PEM_write_bio_PrivateKey(bio, pkey->pkey, cipher, NULL, 0, cb, cb_baton)) { + BIO_free(bio); + err = ERR_get_error(); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "PEM_write key: %ld %s", + err, ERR_error_string(err, NULL)); + return APR_EINVAL; + } + + i = BIO_pending(bio); + if (i > 0) { + buffer->data = apr_palloc(p, (apr_size_t)i + 1); + i = BIO_read(bio, buffer->data, i); + buffer->data[i] = '\0'; + buffer->len = (apr_size_t)i; + } + BIO_free(bio); + return APR_SUCCESS; +} + +apr_status_t md_pkey_fsave(md_pkey_t *pkey, apr_pool_t *p, + const char *pass_phrase, apr_size_t pass_len, + const char *fname, apr_fileperms_t perms) +{ + buffer_rec buffer; + apr_status_t rv; + + if (APR_SUCCESS == (rv = pkey_to_buffer(&buffer, pkey, p, pass_phrase, pass_len))) { + return md_util_freplace(fname, perms, p, fwrite_buffer, &buffer); + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "save pkey %s (%s pass phrase, len=%d)", + fname, pass_len > 0? "with" : "without", (int)pass_len); + return rv; +} + +static apr_status_t gen_rsa(md_pkey_t **ppkey, apr_pool_t *p, unsigned int bits) +{ + EVP_PKEY_CTX *ctx = NULL; + apr_status_t rv; + + *ppkey = make_pkey(p); + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + if (ctx + && EVP_PKEY_keygen_init(ctx) >= 0 + && EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, (int)bits) >= 0 + && EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) >= 0) { + rv = APR_SUCCESS; + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "error generate pkey RSA %d", bits); + *ppkey = NULL; + rv = APR_EGENERAL; + } + + if (ctx != NULL) { + EVP_PKEY_CTX_free(ctx); + } + return rv; +} + +apr_status_t md_pkey_gen(md_pkey_t **ppkey, apr_pool_t *p, md_pkey_spec_t *spec) +{ + md_pkey_type_t ptype = spec? spec->type : MD_PKEY_TYPE_DEFAULT; + switch (ptype) { + case MD_PKEY_TYPE_DEFAULT: + return gen_rsa(ppkey, p, MD_PKEY_RSA_BITS_DEF); + case MD_PKEY_TYPE_RSA: + return gen_rsa(ppkey, p, spec->params.rsa.bits); + default: + return APR_ENOTIMPL; + } +} + +#if MD_USE_OPENSSL_PRE_1_1_API || (defined(LIBRESSL_VERSION_NUMBER) && \ + LIBRESSL_VERSION_NUMBER < 0x2070000f) + +#ifndef NID_tlsfeature +#define NID_tlsfeature 1020 +#endif + +static void RSA_get0_key(const RSA *r, + const BIGNUM **n, const BIGNUM **e, const BIGNUM **d) +{ + if (n != NULL) + *n = r->n; + if (e != NULL) + *e = r->e; + if (d != NULL) + *d = r->d; +} + +#endif + +static const char *bn64(const BIGNUM *b, apr_pool_t *p) +{ + if (b) { + apr_size_t len = (apr_size_t)BN_num_bytes(b); + char *buffer = apr_pcalloc(p, len); + if (buffer) { + BN_bn2bin(b, (unsigned char *)buffer); + return md_util_base64url_encode(buffer, len, p); + } + } + return NULL; +} + +const char *md_pkey_get_rsa_e64(md_pkey_t *pkey, apr_pool_t *p) +{ + const BIGNUM *e; + RSA *rsa = EVP_PKEY_get1_RSA(pkey->pkey); + + if (!rsa) { + return NULL; + } + RSA_get0_key(rsa, NULL, &e, NULL); + return bn64(e, p); +} + +const char *md_pkey_get_rsa_n64(md_pkey_t *pkey, apr_pool_t *p) +{ + const BIGNUM *n; + RSA *rsa = EVP_PKEY_get1_RSA(pkey->pkey); + + if (!rsa) { + return NULL; + } + RSA_get0_key(rsa, &n, NULL, NULL); + return bn64(n, p); +} + +apr_status_t md_crypt_sign64(const char **psign64, md_pkey_t *pkey, apr_pool_t *p, + const char *d, size_t dlen) +{ + EVP_MD_CTX *ctx = NULL; + char *buffer; + unsigned int blen; + const char *sign64 = NULL; + apr_status_t rv = APR_ENOMEM; + + buffer = apr_pcalloc(p, (apr_size_t)EVP_PKEY_size(pkey->pkey)); + if (buffer) { + ctx = EVP_MD_CTX_create(); + if (ctx) { + rv = APR_ENOTIMPL; + if (EVP_SignInit_ex(ctx, EVP_sha256(), NULL)) { + rv = APR_EGENERAL; + if (EVP_SignUpdate(ctx, d, dlen)) { + if (EVP_SignFinal(ctx, (unsigned char*)buffer, &blen, pkey->pkey)) { + sign64 = md_util_base64url_encode(buffer, blen, p); + if (sign64) { + rv = APR_SUCCESS; + } + } + } + } + } + + if (ctx) { + EVP_MD_CTX_destroy(ctx); + } + } + + if (rv != APR_SUCCESS) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "signing"); + } + + *psign64 = sign64; + return rv; +} + +static apr_status_t sha256_digest(unsigned char **pdigest, size_t *pdigest_len, + apr_pool_t *p, const char *d, size_t dlen) +{ + EVP_MD_CTX *ctx = NULL; + unsigned char *buffer; + apr_status_t rv = APR_ENOMEM; + unsigned int blen; + + buffer = apr_pcalloc(p, EVP_MAX_MD_SIZE); + if (buffer) { + ctx = EVP_MD_CTX_create(); + if (ctx) { + rv = APR_ENOTIMPL; + if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL)) { + rv = APR_EGENERAL; + if (EVP_DigestUpdate(ctx, d, dlen)) { + if (EVP_DigestFinal(ctx, buffer, &blen)) { + rv = APR_SUCCESS; + } + } + } + } + + if (ctx) { + EVP_MD_CTX_destroy(ctx); + } + } + + if (APR_SUCCESS == rv) { + *pdigest = buffer; + *pdigest_len = blen; + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "digest"); + *pdigest = NULL; + *pdigest_len = 0; + } + return rv; +} + +apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, + const char *d, size_t dlen) +{ + const char *digest64 = NULL; + unsigned char *buffer; + size_t blen; + apr_status_t rv; + + if (APR_SUCCESS == (rv = sha256_digest(&buffer, &blen, p, d, dlen))) { + if (NULL == (digest64 = md_util_base64url_encode((const char*)buffer, blen, p))) { + rv = APR_EGENERAL; + } + } + *pdigest64 = digest64; + return rv; +} + +static const char * const hex_const[] = { + "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", + "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", + "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", + "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", + "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", + "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", + "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", + "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", + "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", + "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", + "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", + "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", + "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", + "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", + "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", + "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff", +}; + +apr_status_t md_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p, + const char *d, size_t dlen) +{ + char *dhex = NULL, *cp; + const char * x; + unsigned char *buffer; + size_t blen; + apr_status_t rv; + unsigned int i; + + if (APR_SUCCESS == (rv = sha256_digest(&buffer, &blen, p, d, dlen))) { + cp = dhex = apr_pcalloc(p, 2 * blen + 1); + if (!dhex) { + rv = APR_EGENERAL; + } + for (i = 0; i < blen; ++i, cp += 2) { + x = hex_const[buffer[i]]; + cp[0] = x[0]; + cp[1] = x[1]; + } + } + *pdigesthex = dhex; + return rv; +} + +/**************************************************************************************************/ +/* certificates */ + +struct md_cert_t { + apr_pool_t *pool; + X509 *x509; + apr_array_header_t *alt_names; +}; + +static apr_status_t cert_cleanup(void *data) +{ + md_cert_t *cert = data; + if (cert->x509) { + X509_free(cert->x509); + cert->x509 = NULL; + } + return APR_SUCCESS; +} + +static md_cert_t *make_cert(apr_pool_t *p, X509 *x509) +{ + md_cert_t *cert = apr_pcalloc(p, sizeof(*cert)); + cert->pool = p; + cert->x509 = x509; + apr_pool_cleanup_register(p, cert, cert_cleanup, apr_pool_cleanup_null); + + return cert; +} + +void md_cert_free(md_cert_t *cert) +{ + cert_cleanup(cert); +} + +void *md_cert_get_X509(struct md_cert_t *cert) +{ + return cert->x509; +} + +int md_cert_is_valid_now(const md_cert_t *cert) +{ + return ((X509_cmp_current_time(X509_get_notBefore(cert->x509)) < 0) + && (X509_cmp_current_time(X509_get_notAfter(cert->x509)) > 0)); +} + +int md_cert_has_expired(const md_cert_t *cert) +{ + return (X509_cmp_current_time(X509_get_notAfter(cert->x509)) <= 0); +} + +apr_time_t md_cert_get_not_after(md_cert_t *cert) +{ + return md_asn1_time_get(X509_get_notAfter(cert->x509)); +} + +apr_time_t md_cert_get_not_before(md_cert_t *cert) +{ + return md_asn1_time_get(X509_get_notBefore(cert->x509)); +} + +int md_cert_covers_domain(md_cert_t *cert, const char *domain_name) +{ + if (!cert->alt_names) { + md_cert_get_alt_names(&cert->alt_names, cert, cert->pool); + } + if (cert->alt_names) { + return md_array_str_index(cert->alt_names, domain_name, 0, 0) >= 0; + } + return 0; +} + +int md_cert_covers_md(md_cert_t *cert, const md_t *md) +{ + const char *name; + int i; + + if (!cert->alt_names) { + md_cert_get_alt_names(&cert->alt_names, cert, cert->pool); + } + if (cert->alt_names) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, cert->pool, "cert has %d alt names", + cert->alt_names->nelts); + for (i = 0; i < md->domains->nelts; ++i) { + name = APR_ARRAY_IDX(md->domains, i, const char *); + if (md_array_str_index(cert->alt_names, name, 0, 0) < 0) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, cert->pool, + "md domain %s not covered by cert", name); + return 0; + } + } + return 1; + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, cert->pool, "cert has NO alt names"); + } + return 0; +} + +apr_status_t md_cert_get_issuers_uri(const char **puri, md_cert_t *cert, apr_pool_t *p) +{ + apr_status_t rv = APR_ENOENT; + STACK_OF(ACCESS_DESCRIPTION) *xinfos; + const char *uri = NULL; + unsigned char *buf; + int i; + + xinfos = X509_get_ext_d2i(cert->x509, NID_info_access, NULL, NULL); + if (xinfos) { + for (i = 0; i < sk_ACCESS_DESCRIPTION_num(xinfos); i++) { + ACCESS_DESCRIPTION *val = sk_ACCESS_DESCRIPTION_value(xinfos, i); + if (OBJ_obj2nid(val->method) == NID_ad_ca_issuers + && val->location && val->location->type == GEN_URI) { + ASN1_STRING_to_UTF8(&buf, val->location->d.uniformResourceIdentifier); + uri = apr_pstrdup(p, (char *)buf); + OPENSSL_free(buf); + rv = APR_SUCCESS; + break; + } + } + sk_ACCESS_DESCRIPTION_pop_free(xinfos, ACCESS_DESCRIPTION_free); + } + *puri = (APR_SUCCESS == rv)? uri : NULL; + return rv; +} + +apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, md_cert_t *cert, apr_pool_t *p) +{ + apr_array_header_t *names; + apr_status_t rv = APR_ENOENT; + STACK_OF(GENERAL_NAME) *xalt_names; + unsigned char *buf; + int i; + + xalt_names = X509_get_ext_d2i(cert->x509, NID_subject_alt_name, NULL, NULL); + if (xalt_names) { + GENERAL_NAME *cval; + + names = apr_array_make(p, sk_GENERAL_NAME_num(xalt_names), sizeof(char *)); + for (i = 0; i < sk_GENERAL_NAME_num(xalt_names); ++i) { + cval = sk_GENERAL_NAME_value(xalt_names, i); + switch (cval->type) { + case GEN_DNS: + case GEN_URI: + case GEN_IPADD: + ASN1_STRING_to_UTF8(&buf, cval->d.ia5); + APR_ARRAY_PUSH(names, const char *) = apr_pstrdup(p, (char*)buf); + OPENSSL_free(buf); + break; + default: + break; + } + } + sk_GENERAL_NAME_pop_free(xalt_names, GENERAL_NAME_free); + rv = APR_SUCCESS; + } + *pnames = (APR_SUCCESS == rv)? names : NULL; + return rv; +} + +apr_status_t md_cert_fload(md_cert_t **pcert, apr_pool_t *p, const char *fname) +{ + FILE *f; + apr_status_t rv; + md_cert_t *cert; + X509 *x509; + + rv = md_util_fopen(&f, fname, "r"); + if (rv == APR_SUCCESS) { + + x509 = PEM_read_X509(f, NULL, NULL, NULL); + rv = fclose(f); + if (x509 != NULL) { + cert = make_cert(p, x509); + } + else { + rv = APR_EINVAL; + } + } + + *pcert = (APR_SUCCESS == rv)? cert : NULL; + return rv; +} + +static apr_status_t cert_to_buffer(buffer_rec *buffer, md_cert_t *cert, apr_pool_t *p) +{ + BIO *bio = BIO_new(BIO_s_mem()); + int i; + + if (!bio) { + return APR_ENOMEM; + } + + ERR_clear_error(); + PEM_write_bio_X509(bio, cert->x509); + if (ERR_get_error() > 0) { + BIO_free(bio); + return APR_EINVAL; + } + + i = BIO_pending(bio); + if (i > 0) { + buffer->data = apr_palloc(p, (apr_size_t)i + 1); + i = BIO_read(bio, buffer->data, i); + buffer->data[i] = '\0'; + buffer->len = (apr_size_t)i; + } + BIO_free(bio); + return APR_SUCCESS; +} + +apr_status_t md_cert_fsave(md_cert_t *cert, apr_pool_t *p, + const char *fname, apr_fileperms_t perms) +{ + buffer_rec buffer; + apr_status_t rv; + + if (APR_SUCCESS == (rv = cert_to_buffer(&buffer, cert, p))) { + return md_util_freplace(fname, perms, p, fwrite_buffer, &buffer); + } + return rv; +} + +apr_status_t md_cert_to_base64url(const char **ps64, md_cert_t *cert, apr_pool_t *p) +{ + buffer_rec buffer; + apr_status_t rv; + + if (APR_SUCCESS == (rv = cert_to_buffer(&buffer, cert, p))) { + *ps64 = md_util_base64url_encode(buffer.data, buffer.len, p); + return APR_SUCCESS; + } + *ps64 = NULL; + return rv; +} + +apr_status_t md_cert_read_http(md_cert_t **pcert, apr_pool_t *p, + const md_http_response_t *res) +{ + const char *ct; + apr_off_t data_len; + apr_size_t der_len; + apr_status_t rv; + + ct = apr_table_get(res->headers, "Content-Type"); + if (!res->body || !ct || strcmp("application/pkix-cert", ct)) { + return APR_ENOENT; + } + + if (APR_SUCCESS == (rv = apr_brigade_length(res->body, 1, &data_len))) { + char *der; + if (data_len > 1024*1024) { /* certs usually are <2k each */ + return APR_EINVAL; + } + if (APR_SUCCESS == (rv = apr_brigade_pflatten(res->body, &der, &der_len, p))) { + const unsigned char *bf = (const unsigned char*)der; + X509 *x509; + + if (NULL == (x509 = d2i_X509(NULL, &bf, (long)der_len))) { + rv = APR_EINVAL; + } + else { + *pcert = make_cert(p, x509); + rv = APR_SUCCESS; + } + } + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "cert parsed"); + } + return rv; +} + +md_cert_state_t md_cert_state_get(md_cert_t *cert) +{ + if (cert->x509) { + return md_cert_is_valid_now(cert)? MD_CERT_VALID : MD_CERT_EXPIRED; + } + return MD_CERT_UNKNOWN; +} + +apr_status_t md_chain_fappend(struct apr_array_header_t *certs, apr_pool_t *p, const char *fname) +{ + FILE *f; + apr_status_t rv; + X509 *x509; + md_cert_t *cert; + unsigned long err; + + rv = md_util_fopen(&f, fname, "r"); + if (rv == APR_SUCCESS) { + ERR_clear_error(); + while (NULL != (x509 = PEM_read_X509(f, NULL, NULL, NULL))) { + cert = make_cert(p, x509); + APR_ARRAY_PUSH(certs, md_cert_t *) = cert; + } + fclose(f); + + if (0 < (err = ERR_get_error()) + && !(ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_NO_START_LINE)) { + /* not the expected one when no more PEM encodings are found */ + rv = APR_EINVAL; + goto out; + } + + if (certs->nelts == 0) { + /* Did not find any. This is acceptable unless the file has a certain size + * when we no longer accept it as empty chain file. Something seems to be + * wrong then. */ + apr_finfo_t info; + if (APR_SUCCESS == apr_stat(&info, fname, APR_FINFO_SIZE, p) && info.size >= 1024) { + /* "Too big for a moon." */ + rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, + "no certificates in non-empty chain %s", fname); + goto out; + } + } + } +out: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "read chain file %s, found %d certs", + fname, certs? certs->nelts : 0); + return rv; +} + +apr_status_t md_chain_fload(apr_array_header_t **pcerts, apr_pool_t *p, const char *fname) +{ + apr_array_header_t *certs; + apr_status_t rv; + + certs = apr_array_make(p, 5, sizeof(md_cert_t *)); + rv = md_chain_fappend(certs, p, fname); + *pcerts = (APR_SUCCESS == rv)? certs : NULL; + return rv; +} + +apr_status_t md_chain_fsave(apr_array_header_t *certs, apr_pool_t *p, + const char *fname, apr_fileperms_t perms) +{ + FILE *f; + apr_status_t rv; + const md_cert_t *cert; + unsigned long err = 0; + int i; + + (void)p; + rv = md_util_fopen(&f, fname, "w"); + if (rv == APR_SUCCESS) { + apr_file_perms_set(fname, perms); + ERR_clear_error(); + for (i = 0; i < certs->nelts; ++i) { + cert = APR_ARRAY_IDX(certs, i, const md_cert_t *); + assert(cert->x509); + + PEM_write_X509(f, cert->x509); + + if (0 < (err = ERR_get_error())) { + break; + } + + } + rv = fclose(f); + if (err) { + rv = APR_EINVAL; + } + } + return rv; +} + +/**************************************************************************************************/ +/* certificate signing requests */ + +static const char *alt_names(apr_array_header_t *domains, apr_pool_t *p) +{ + const char *alts = "", *sep = "", *domain; + int i; + + for (i = 0; i < domains->nelts; ++i) { + domain = APR_ARRAY_IDX(domains, i, const char *); + alts = apr_psprintf(p, "%s%sDNS:%s", alts, sep, domain); + sep = ","; + } + return alts; +} + +static apr_status_t add_ext(X509 *x, int nid, const char *value, apr_pool_t *p) +{ + X509_EXTENSION *ext = NULL; + X509V3_CTX ctx; + apr_status_t rv; + + X509V3_set_ctx_nodb(&ctx); + X509V3_set_ctx(&ctx, x, x, NULL, NULL, 0); + if (NULL == (ext = X509V3_EXT_conf_nid(NULL, &ctx, nid, (char*)value))) { + return APR_EGENERAL; + } + + ERR_clear_error(); + rv = X509_add_ext(x, ext, -1)? APR_SUCCESS : APR_EINVAL; + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "add_ext nid=%dd value='%s'", + nid, value); + + } + X509_EXTENSION_free(ext); + return rv; +} + +static apr_status_t sk_add_alt_names(STACK_OF(X509_EXTENSION) *exts, + apr_array_header_t *domains, apr_pool_t *p) +{ + if (domains->nelts > 0) { + X509_EXTENSION *x; + + x = X509V3_EXT_conf_nid(NULL, NULL, NID_subject_alt_name, (char*)alt_names(domains, p)); + if (NULL == x) { + return APR_EGENERAL; + } + sk_X509_EXTENSION_push(exts, x); + } + return APR_SUCCESS; +} + +#define MD_OID_MUST_STAPLE_NUM "1.3.6.1.5.5.7.1.24" +#define MD_OID_MUST_STAPLE_SNAME "tlsfeature" +#define MD_OID_MUST_STAPLE_LNAME "TLS Feature" + +static int get_must_staple_nid(void) +{ + /* Funny API, the OID for must staple might be configured or + * might be not. In the second case, we need to add it. But adding + * when it already is there is an error... */ + int nid = OBJ_txt2nid(MD_OID_MUST_STAPLE_NUM); + if (NID_undef == nid) { + nid = OBJ_create(MD_OID_MUST_STAPLE_NUM, + MD_OID_MUST_STAPLE_SNAME, MD_OID_MUST_STAPLE_LNAME); + } + return nid; +} + +int md_cert_must_staple(md_cert_t *cert) +{ + /* In case we do not get the NID for it, we treat this as not set. */ + int nid = get_must_staple_nid(); + return ((NID_undef != nid)) && X509_get_ext_by_NID(cert->x509, nid, -1) >= 0; +} + +static apr_status_t add_must_staple(STACK_OF(X509_EXTENSION) *exts, const md_t *md, apr_pool_t *p) +{ + + if (md->must_staple) { + X509_EXTENSION *x; + int nid; + + nid = get_must_staple_nid(); + if (NID_undef == nid) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "%s: unable to get NID for v3 must-staple TLS feature", md->name); + return APR_ENOTIMPL; + } + x = X509V3_EXT_conf_nid(NULL, NULL, nid, (char*)"DER:30:03:02:01:05"); + if (NULL == x) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "%s: unable to create x509 extension for must-staple", md->name); + return APR_EGENERAL; + } + sk_X509_EXTENSION_push(exts, x); + } + return APR_SUCCESS; +} + +apr_status_t md_cert_req_create(const char **pcsr_der_64, const md_t *md, + md_pkey_t *pkey, apr_pool_t *p) +{ + const char *s, *csr_der, *csr_der_64 = NULL; + const unsigned char *domain; + X509_REQ *csr; + X509_NAME *n = NULL; + STACK_OF(X509_EXTENSION) *exts = NULL; + apr_status_t rv; + int csr_der_len; + + assert(md->domains->nelts > 0); + + if (NULL == (csr = X509_REQ_new()) + || NULL == (exts = sk_X509_EXTENSION_new_null()) + || NULL == (n = X509_NAME_new())) { + rv = APR_ENOMEM; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: openssl alloc X509 things", md->name); + goto out; + } + + /* subject name == first domain */ + domain = APR_ARRAY_IDX(md->domains, 0, const unsigned char *); + if (!X509_NAME_add_entry_by_txt(n, "CN", MBSTRING_ASC, domain, -1, -1, 0) + || !X509_REQ_set_subject_name(csr, n)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: REQ name add entry", md->name); + rv = APR_EGENERAL; goto out; + } + /* collect extensions, such as alt names and must staple */ + if (APR_SUCCESS != (rv = sk_add_alt_names(exts, md->domains, p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: collecting alt names", md->name); + rv = APR_EGENERAL; goto out; + } + if (APR_SUCCESS != (rv = add_must_staple(exts, md, p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: you requested that a certificate " + "is created with the 'must-staple' extension, however the SSL library was " + "unable to initialized that extension. Please file a bug report on which platform " + "and with which library this happens. To continue before this problem is resolved, " + "configure 'MDMustStaple off' for your domains", md->name); + rv = APR_EGENERAL; goto out; + } + /* add extensions to csr */ + if (sk_X509_EXTENSION_num(exts) > 0 && !X509_REQ_add_extensions(csr, exts)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: adding exts", md->name); + rv = APR_EGENERAL; goto out; + } + /* add our key */ + if (!X509_REQ_set_pubkey(csr, pkey->pkey)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set pkey in csr", md->name); + rv = APR_EGENERAL; goto out; + } + /* sign, der encode and base64url encode */ + if (!X509_REQ_sign(csr, pkey->pkey, EVP_sha256())) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign csr", md->name); + rv = APR_EGENERAL; goto out; + } + if ((csr_der_len = i2d_X509_REQ(csr, NULL)) < 0) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: der length", md->name); + rv = APR_EGENERAL; goto out; + } + s = csr_der = apr_pcalloc(p, (apr_size_t)csr_der_len + 1); + if (i2d_X509_REQ(csr, (unsigned char**)&s) != csr_der_len) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: csr der enc", md->name); + rv = APR_EGENERAL; goto out; + } + csr_der_64 = md_util_base64url_encode(csr_der, (apr_size_t)csr_der_len, p); + rv = APR_SUCCESS; + +out: + if (exts) { + sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free); + } + if (csr) { + X509_REQ_free(csr); + } + if (n) { + X509_NAME_free(n); + } + *pcsr_der_64 = (APR_SUCCESS == rv)? csr_der_64 : NULL; + return rv; +} + +apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, + apr_array_header_t *domains, md_pkey_t *pkey, + apr_interval_time_t valid_for, apr_pool_t *p) +{ + X509 *x; + X509_NAME *n = NULL; + md_cert_t *cert = NULL; + apr_status_t rv; + int days; + BIGNUM *big_rnd = NULL; + ASN1_INTEGER *asn1_rnd = NULL; + unsigned char rnd[20]; + + assert(domains); + + if (NULL == (x = X509_new()) + || NULL == (n = X509_NAME_new())) { + rv = APR_ENOMEM; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: openssl alloc X509 things", cn); + goto out; + } + + if (APR_SUCCESS != (rv = md_rand_bytes(rnd, sizeof(rnd), p)) + || !(big_rnd = BN_bin2bn(rnd, sizeof(rnd), NULL)) + || !(asn1_rnd = BN_to_ASN1_INTEGER(big_rnd, NULL))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: setup random serial", cn); + rv = APR_EGENERAL; goto out; + } + + if (1 != X509_set_version(x, 2L)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: setting x.509v3", cn); + rv = APR_EGENERAL; goto out; + } + + if (!X509_set_serialNumber(x, asn1_rnd)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: set serial number", cn); + rv = APR_EGENERAL; goto out; + } + /* set common name and issue */ + if (!X509_NAME_add_entry_by_txt(n, "CN", MBSTRING_ASC, (const unsigned char*)cn, -1, -1, 0) + || !X509_set_subject_name(x, n) + || !X509_set_issuer_name(x, n)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: name add entry", cn); + rv = APR_EGENERAL; goto out; + } + /* cert are unconstrained (but not very trustworthy) */ + if (APR_SUCCESS != (rv = add_ext(x, NID_basic_constraints, "CA:FALSE, pathlen:0", p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set basic constraints ext", cn); + goto out; + } + /* add the domain as alt name */ + if (APR_SUCCESS != (rv = add_ext(x, NID_subject_alt_name, alt_names(domains, p), p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set alt_name ext", cn); + goto out; + } + /* add our key */ + if (!X509_set_pubkey(x, pkey->pkey)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set pkey in x509", cn); + rv = APR_EGENERAL; goto out; + } + + days = (int)((apr_time_sec(valid_for) + MD_SECS_PER_DAY - 1)/ MD_SECS_PER_DAY); + if (!X509_set_notBefore(x, ASN1_TIME_set(NULL, time(NULL)))) { + rv = APR_EGENERAL; goto out; + } + if (!X509_set_notAfter(x, ASN1_TIME_adj(NULL, time(NULL), days, 0))) { + rv = APR_EGENERAL; goto out; + } + + /* sign with same key */ + if (!X509_sign(x, pkey->pkey, EVP_sha256())) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign x509", cn); + rv = APR_EGENERAL; goto out; + } + + cert = make_cert(p, x); + rv = APR_SUCCESS; + +out: + if (!cert && x) { + X509_free(x); + } + if (n) { + X509_NAME_free(n); + } + if (big_rnd) { + BN_free(big_rnd); + } + if (asn1_rnd) { + ASN1_INTEGER_free(asn1_rnd); + } + *pcert = (APR_SUCCESS == rv)? cert : NULL; + return rv; +} + diff --git a/modules/md/md_crypt.h b/modules/md/md_crypt.h new file mode 100644 index 0000000..e03c296 --- /dev/null +++ b/modules/md/md_crypt.h @@ -0,0 +1,135 @@ +/* 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. + */ + +#ifndef mod_md_md_crypt_h +#define mod_md_md_crypt_h + +#include <apr_file_io.h> + +struct apr_array_header_t; +struct md_t; +struct md_http_response_t; +struct md_cert_t; +struct md_pkey_t; + +/**************************************************************************************************/ +/* random */ + +apr_status_t md_rand_bytes(unsigned char *buf, apr_size_t len, apr_pool_t *p); + +/**************************************************************************************************/ +/* digests */ +apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, + const char *d, size_t dlen); +apr_status_t md_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p, + const char *d, size_t dlen); + +/**************************************************************************************************/ +/* private keys */ + +typedef struct md_pkey_t md_pkey_t; + +typedef enum { + MD_PKEY_TYPE_DEFAULT, + MD_PKEY_TYPE_RSA, +} md_pkey_type_t; + +typedef struct md_pkey_rsa_spec_t { + apr_uint32_t bits; +} md_pkey_rsa_spec_t; + +typedef struct md_pkey_spec_t { + md_pkey_type_t type; + union { + md_pkey_rsa_spec_t rsa; + } params; +} md_pkey_spec_t; + +apr_status_t md_crypt_init(apr_pool_t *pool); + +apr_status_t md_pkey_gen(md_pkey_t **ppkey, apr_pool_t *p, md_pkey_spec_t *spec); +void md_pkey_free(md_pkey_t *pkey); + +const char *md_pkey_get_rsa_e64(md_pkey_t *pkey, apr_pool_t *p); +const char *md_pkey_get_rsa_n64(md_pkey_t *pkey, apr_pool_t *p); + +apr_status_t md_pkey_fload(md_pkey_t **ppkey, apr_pool_t *p, + const char *pass_phrase, apr_size_t pass_len, + const char *fname); +apr_status_t md_pkey_fsave(md_pkey_t *pkey, apr_pool_t *p, + const char *pass_phrase, apr_size_t pass_len, + const char *fname, apr_fileperms_t perms); + +apr_status_t md_crypt_sign64(const char **psign64, md_pkey_t *pkey, apr_pool_t *p, + const char *d, size_t dlen); + +void *md_cert_get_X509(struct md_cert_t *cert); +void *md_pkey_get_EVP_PKEY(struct md_pkey_t *pkey); + +struct md_json_t *md_pkey_spec_to_json(const md_pkey_spec_t *spec, apr_pool_t *p); +md_pkey_spec_t *md_pkey_spec_from_json(struct md_json_t *json, apr_pool_t *p); +int md_pkey_spec_eq(md_pkey_spec_t *spec1, md_pkey_spec_t *spec2); + +/**************************************************************************************************/ +/* X509 certificates */ + +typedef struct md_cert_t md_cert_t; + +typedef enum { + MD_CERT_UNKNOWN, + MD_CERT_VALID, + MD_CERT_EXPIRED +} md_cert_state_t; + +void md_cert_free(md_cert_t *cert); + +apr_status_t md_cert_fload(md_cert_t **pcert, apr_pool_t *p, const char *fname); +apr_status_t md_cert_fsave(md_cert_t *cert, apr_pool_t *p, + const char *fname, apr_fileperms_t perms); + +apr_status_t md_cert_read_http(md_cert_t **pcert, apr_pool_t *pool, + const struct md_http_response_t *res); + +md_cert_state_t md_cert_state_get(md_cert_t *cert); +int md_cert_is_valid_now(const md_cert_t *cert); +int md_cert_has_expired(const md_cert_t *cert); +int md_cert_covers_domain(md_cert_t *cert, const char *domain_name); +int md_cert_covers_md(md_cert_t *cert, const struct md_t *md); +int md_cert_must_staple(md_cert_t *cert); +apr_time_t md_cert_get_not_after(md_cert_t *cert); +apr_time_t md_cert_get_not_before(md_cert_t *cert); + +apr_status_t md_cert_get_issuers_uri(const char **puri, md_cert_t *cert, apr_pool_t *p); +apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, md_cert_t *cert, apr_pool_t *p); + +apr_status_t md_cert_to_base64url(const char **ps64, md_cert_t *cert, apr_pool_t *p); +apr_status_t md_cert_from_base64url(md_cert_t **pcert, const char *s64, apr_pool_t *p); + +apr_status_t md_chain_fload(struct apr_array_header_t **pcerts, + apr_pool_t *p, const char *fname); +apr_status_t md_chain_fsave(struct apr_array_header_t *certs, + apr_pool_t *p, const char *fname, apr_fileperms_t perms); +apr_status_t md_chain_fappend(struct apr_array_header_t *certs, + apr_pool_t *p, const char *fname); + +apr_status_t md_cert_req_create(const char **pcsr_der_64, const struct md_t *md, + md_pkey_t *pkey, apr_pool_t *p); + +apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, + struct apr_array_header_t *domains, md_pkey_t *pkey, + apr_interval_time_t valid_for, apr_pool_t *p); + +#endif /* md_crypt_h */ diff --git a/modules/md/md_curl.c b/modules/md/md_curl.c new file mode 100644 index 0000000..f3585da --- /dev/null +++ b/modules/md/md_curl.c @@ -0,0 +1,307 @@ +/* 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. + */ + +#include <assert.h> + +#include <curl/curl.h> + +#include <apr_lib.h> +#include <apr_strings.h> +#include <apr_buckets.h> + +#include "md_http.h" +#include "md_log.h" +#include "md_curl.h" + +/**************************************************************************************************/ +/* md_http curl implementation */ + + +static apr_status_t curl_status(int curl_code) +{ + switch (curl_code) { + case CURLE_OK: return APR_SUCCESS; + case CURLE_UNSUPPORTED_PROTOCOL: return APR_ENOTIMPL; + case CURLE_NOT_BUILT_IN: return APR_ENOTIMPL; + case CURLE_URL_MALFORMAT: return APR_EINVAL; + case CURLE_COULDNT_RESOLVE_PROXY:return APR_ECONNREFUSED; + case CURLE_COULDNT_RESOLVE_HOST: return APR_ECONNREFUSED; + case CURLE_COULDNT_CONNECT: return APR_ECONNREFUSED; + case CURLE_REMOTE_ACCESS_DENIED: return APR_EACCES; + case CURLE_OUT_OF_MEMORY: return APR_ENOMEM; + case CURLE_OPERATION_TIMEDOUT: return APR_TIMEUP; + case CURLE_SSL_CONNECT_ERROR: return APR_ECONNABORTED; + case CURLE_AGAIN: return APR_EAGAIN; + default: return APR_EGENERAL; + } +} + +static size_t req_data_cb(void *data, size_t len, size_t nmemb, void *baton) +{ + apr_bucket_brigade *body = baton; + size_t blen, read_len = 0, max_len = len * nmemb; + const char *bdata; + apr_bucket *b; + apr_status_t rv; + + while (body && !APR_BRIGADE_EMPTY(body) && max_len > 0) { + b = APR_BRIGADE_FIRST(body); + if (APR_BUCKET_IS_METADATA(b)) { + if (APR_BUCKET_IS_EOS(b)) { + body = NULL; + } + } + else { + rv = apr_bucket_read(b, &bdata, &blen, APR_BLOCK_READ); + if (rv == APR_SUCCESS) { + if (blen > max_len) { + apr_bucket_split(b, max_len); + blen = max_len; + } + memcpy(data, bdata, blen); + read_len += blen; + max_len -= blen; + } + else { + body = NULL; + if (!APR_STATUS_IS_EOF(rv)) { + /* everything beside EOF is an error */ + read_len = CURL_READFUNC_ABORT; + } + } + + } + apr_bucket_delete(b); + } + + return read_len; +} + +static size_t resp_data_cb(void *data, size_t len, size_t nmemb, void *baton) +{ + md_http_response_t *res = baton; + size_t blen = len * nmemb; + apr_status_t rv; + + if (res->body) { + if (res->req->resp_limit) { + apr_off_t body_len = 0; + apr_brigade_length(res->body, 0, &body_len); + if (body_len + (apr_off_t)len > res->req->resp_limit) { + return 0; /* signal curl failure */ + } + } + rv = apr_brigade_write(res->body, NULL, NULL, (const char *)data, blen); + if (rv != APR_SUCCESS) { + /* returning anything != blen will make CURL fail this */ + return 0; + } + } + return blen; +} + +static size_t header_cb(void *buffer, size_t elen, size_t nmemb, void *baton) +{ + md_http_response_t *res = baton; + size_t len, clen = elen * nmemb; + const char *name = NULL, *value = "", *b = buffer; + apr_size_t i; + + len = (clen && b[clen-1] == '\n')? clen-1 : clen; + len = (len && b[len-1] == '\r')? len-1 : len; + for (i = 0; i < len; ++i) { + if (b[i] == ':') { + name = apr_pstrndup(res->req->pool, b, i); + ++i; + while (i < len && b[i] == ' ') { + ++i; + } + if (i < len) { + value = apr_pstrndup(res->req->pool, b+i, len - i); + } + break; + } + } + + if (name != NULL) { + apr_table_add(res->headers, name, value); + } + return clen; +} + +static apr_status_t curl_init(md_http_request_t *req) +{ + CURL *curl = curl_easy_init(); + if (!curl) { + return APR_EGENERAL; + } + + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, NULL); + curl_easy_setopt(curl, CURLOPT_READFUNCTION, req_data_cb); + curl_easy_setopt(curl, CURLOPT_READDATA, NULL); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, resp_data_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, NULL); + + req->internals = curl; + return APR_SUCCESS; +} + +typedef struct { + md_http_request_t *req; + struct curl_slist *hdrs; + apr_status_t rv; +} curlify_hdrs_ctx; + +static int curlify_headers(void *baton, const char *key, const char *value) +{ + curlify_hdrs_ctx *ctx = baton; + const char *s; + + if (strchr(key, '\r') || strchr(key, '\n') + || strchr(value, '\r') || strchr(value, '\n')) { + ctx->rv = APR_EINVAL; + return 0; + } + s = apr_psprintf(ctx->req->pool, "%s: %s", key, value); + ctx->hdrs = curl_slist_append(ctx->hdrs, s); + return 1; +} + +static apr_status_t curl_perform(md_http_request_t *req) +{ + apr_status_t rv = APR_SUCCESS; + CURLcode curle; + md_http_response_t *res; + CURL *curl; + struct curl_slist *req_hdrs = NULL; + + rv = curl_init(req); + curl = req->internals; + + res = apr_pcalloc(req->pool, sizeof(*res)); + + res->req = req; + res->rv = APR_SUCCESS; + res->status = 400; + res->headers = apr_table_make(req->pool, 5); + res->body = apr_brigade_create(req->pool, req->bucket_alloc); + + curl_easy_setopt(curl, CURLOPT_URL, req->url); + if (!apr_strnatcasecmp("GET", req->method)) { + /* nop */ + } + else if (!apr_strnatcasecmp("HEAD", req->method)) { + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + } + else if (!apr_strnatcasecmp("POST", req->method)) { + curl_easy_setopt(curl, CURLOPT_POST, 1L); + } + else { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->method); + } + curl_easy_setopt(curl, CURLOPT_HEADERDATA, res); + curl_easy_setopt(curl, CURLOPT_READDATA, req->body); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, res); + + if (req->user_agent) { + curl_easy_setopt(curl, CURLOPT_USERAGENT, req->user_agent); + } + if (req->proxy_url) { + curl_easy_setopt(curl, CURLOPT_PROXY, req->proxy_url); + } + if (!apr_is_empty_table(req->headers)) { + curlify_hdrs_ctx ctx; + + ctx.req = req; + ctx.hdrs = NULL; + ctx.rv = APR_SUCCESS; + apr_table_do(curlify_headers, &ctx, req->headers, NULL); + req_hdrs = ctx.hdrs; + if (ctx.rv == APR_SUCCESS) { + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_hdrs); + } + } + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->pool, + "request %ld --> %s %s", req->id, req->method, req->url); + + if (md_log_is_level(req->pool, MD_LOG_TRACE3)) { + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + } + + curle = curl_easy_perform(curl); + res->rv = curl_status(curle); + + if (APR_SUCCESS == res->rv) { + long l; + res->rv = curl_status(curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &l)); + if (APR_SUCCESS == res->rv) { + res->status = (int)l; + } + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, res->rv, req->pool, + "request %ld <-- %d", req->id, res->status); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, res->rv, req->pool, + "request %ld failed(%d): %s", req->id, curle, + curl_easy_strerror(curle)); + } + + if (req->cb) { + res->rv = req->cb(res); + } + + rv = res->rv; + md_http_req_destroy(req); + if (req_hdrs) { + curl_slist_free_all(req_hdrs); + } + + return rv; +} + +static int initialized; + +static apr_status_t md_curl_init(void) { + if (!initialized) { + initialized = 1; + curl_global_init(CURL_GLOBAL_DEFAULT); + } + return APR_SUCCESS; +} + +static void curl_req_cleanup(md_http_request_t *req) +{ + if (req->internals) { + curl_easy_cleanup(req->internals); + req->internals = NULL; + } +} + +static md_http_impl_t impl = { + md_curl_init, + curl_req_cleanup, + curl_perform +}; + +md_http_impl_t * md_curl_get_impl(apr_pool_t *p) +{ + /* trigger early global curl init, before we are down a rabbit hole */ + (void)p; + md_curl_init(); + return &impl; +} diff --git a/modules/md/md_curl.h b/modules/md/md_curl.h new file mode 100644 index 0000000..cbc1dd2 --- /dev/null +++ b/modules/md/md_curl.h @@ -0,0 +1,24 @@ +/* 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. + */ + +#ifndef md_curl_h +#define md_curl_h + +struct md_http_impl; + +struct md_http_impl_t * md_curl_get_impl(apr_pool_t *p); + +#endif /* md_curl_h */ diff --git a/modules/md/md_http.c b/modules/md/md_http.c new file mode 100644 index 0000000..310fc55 --- /dev/null +++ b/modules/md/md_http.c @@ -0,0 +1,245 @@ +/* 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. + */ + +#include <assert.h> + +#include <apr_lib.h> +#include <apr_strings.h> +#include <apr_buckets.h> + +#include "md_http.h" +#include "md_log.h" + +struct md_http_t { + apr_pool_t *pool; + apr_bucket_alloc_t *bucket_alloc; + apr_off_t resp_limit; + md_http_impl_t *impl; + const char *user_agent; + const char *proxy_url; +}; + +static md_http_impl_t *cur_impl; +static int cur_init_done; + +void md_http_use_implementation(md_http_impl_t *impl) +{ + if (cur_impl != impl) { + cur_impl = impl; + cur_init_done = 0; + } +} + +static long next_req_id; + +apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_agent, + const char *proxy_url) +{ + md_http_t *http; + apr_status_t rv = APR_SUCCESS; + + if (!cur_impl) { + *phttp = NULL; + return APR_ENOTIMPL; + } + + if (!cur_init_done) { + if (APR_SUCCESS == (rv = cur_impl->init())) { + cur_init_done = 1; + } + else { + return rv; + } + } + + http = apr_pcalloc(p, sizeof(*http)); + http->pool = p; + http->impl = cur_impl; + http->user_agent = apr_pstrdup(p, user_agent); + http->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL; + http->bucket_alloc = apr_bucket_alloc_create(p); + if (!http->bucket_alloc) { + return APR_EGENERAL; + } + *phttp = http; + return APR_SUCCESS; +} + +void md_http_set_response_limit(md_http_t *http, apr_off_t resp_limit) +{ + http->resp_limit = resp_limit; +} + +static apr_status_t req_create(md_http_request_t **preq, md_http_t *http, + const char *method, const char *url, struct apr_table_t *headers, + md_http_cb *cb, void *baton) +{ + md_http_request_t *req; + apr_pool_t *pool; + apr_status_t rv; + + rv = apr_pool_create(&pool, http->pool); + if (rv != APR_SUCCESS) { + return rv; + } + + req = apr_pcalloc(pool, sizeof(*req)); + req->id = next_req_id++; + req->pool = pool; + req->bucket_alloc = http->bucket_alloc; + req->http = http; + req->method = method; + req->url = url; + req->headers = headers? apr_table_copy(req->pool, headers) : apr_table_make(req->pool, 5); + req->resp_limit = http->resp_limit; + req->cb = cb; + req->baton = baton; + req->user_agent = http->user_agent; + req->proxy_url = http->proxy_url; + + *preq = req; + return rv; +} + +void md_http_req_destroy(md_http_request_t *req) +{ + if (req->internals) { + req->http->impl->req_cleanup(req); + req->internals = NULL; + } + apr_pool_destroy(req->pool); +} + +static apr_status_t schedule(md_http_request_t *req, + apr_bucket_brigade *body, int detect_clen, + long *preq_id) +{ + apr_status_t rv; + + req->body = body; + req->body_len = body? -1 : 0; + + if (req->body && detect_clen) { + rv = apr_brigade_length(req->body, 1, &req->body_len); + if (rv != APR_SUCCESS) { + md_http_req_destroy(req); + return rv; + } + } + + if (req->body_len == 0 && apr_strnatcasecmp("GET", req->method)) { + apr_table_setn(req->headers, "Content-Length", "0"); + } + else if (req->body_len > 0) { + apr_table_setn(req->headers, "Content-Length", apr_off_t_toa(req->pool, req->body_len)); + } + + if (preq_id) { + *preq_id = req->id; + } + + /* we send right away */ + rv = req->http->impl->perform(req); + + return rv; +} + +apr_status_t md_http_GET(struct md_http_t *http, + const char *url, struct apr_table_t *headers, + md_http_cb *cb, void *baton, long *preq_id) +{ + md_http_request_t *req; + apr_status_t rv; + + rv = req_create(&req, http, "GET", url, headers, cb, baton); + if (rv != APR_SUCCESS) { + return rv; + } + + return schedule(req, NULL, 0, preq_id); +} + +apr_status_t md_http_HEAD(struct md_http_t *http, + const char *url, struct apr_table_t *headers, + md_http_cb *cb, void *baton, long *preq_id) +{ + md_http_request_t *req; + apr_status_t rv; + + rv = req_create(&req, http, "HEAD", url, headers, cb, baton); + if (rv != APR_SUCCESS) { + return rv; + } + + return schedule(req, NULL, 0, preq_id); +} + +apr_status_t md_http_POST(struct md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + apr_bucket_brigade *body, + md_http_cb *cb, void *baton, long *preq_id) +{ + md_http_request_t *req; + apr_status_t rv; + + rv = req_create(&req, http, "POST", url, headers, cb, baton); + if (rv != APR_SUCCESS) { + return rv; + } + + if (content_type) { + apr_table_set(req->headers, "Content-Type", content_type); + } + return schedule(req, body, 1, preq_id); +} + +apr_status_t md_http_POSTd(md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + const char *data, size_t data_len, + md_http_cb *cb, void *baton, long *preq_id) +{ + md_http_request_t *req; + apr_status_t rv; + apr_bucket_brigade *body = NULL; + + rv = req_create(&req, http, "POST", url, headers, cb, baton); + if (rv != APR_SUCCESS) { + return rv; + } + + if (data && data_len > 0) { + body = apr_brigade_create(req->pool, req->http->bucket_alloc); + rv = apr_brigade_write(body, NULL, NULL, data, data_len); + if (rv != APR_SUCCESS) { + md_http_req_destroy(req); + return rv; + } + } + + if (content_type) { + apr_table_set(req->headers, "Content-Type", content_type); + } + + return schedule(req, body, 1, preq_id); +} + +apr_status_t md_http_await(md_http_t *http, long req_id) +{ + (void)http; + (void)req_id; + return APR_SUCCESS; +} + diff --git a/modules/md/md_http.h b/modules/md/md_http.h new file mode 100644 index 0000000..c6d94bb --- /dev/null +++ b/modules/md/md_http.h @@ -0,0 +1,102 @@ +/* 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. + */ + +#ifndef mod_md_md_http_h +#define mod_md_md_http_h + +struct apr_table_t; +struct apr_bucket_brigade; +struct apr_bucket_alloc_t; + +typedef struct md_http_t md_http_t; + +typedef struct md_http_request_t md_http_request_t; +typedef struct md_http_response_t md_http_response_t; + +typedef apr_status_t md_http_cb(const md_http_response_t *res); + +struct md_http_request_t { + long id; + md_http_t *http; + apr_pool_t *pool; + struct apr_bucket_alloc_t *bucket_alloc; + const char *method; + const char *url; + const char *user_agent; + const char *proxy_url; + apr_table_t *headers; + struct apr_bucket_brigade *body; + apr_off_t body_len; + apr_off_t resp_limit; + md_http_cb *cb; + void *baton; + void *internals; +}; + +struct md_http_response_t { + md_http_request_t *req; + apr_status_t rv; + int status; + apr_table_t *headers; + struct apr_bucket_brigade *body; +}; + +apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_agent, + const char *proxy_url); + +void md_http_set_response_limit(md_http_t *http, apr_off_t resp_limit); + +apr_status_t md_http_GET(md_http_t *http, + const char *url, struct apr_table_t *headers, + md_http_cb *cb, void *baton, long *preq_id); + +apr_status_t md_http_HEAD(md_http_t *http, + const char *url, struct apr_table_t *headers, + md_http_cb *cb, void *baton, long *preq_id); + +apr_status_t md_http_POST(md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + struct apr_bucket_brigade *body, + md_http_cb *cb, void *baton, long *preq_id); + +apr_status_t md_http_POSTd(md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + const char *data, size_t data_len, + md_http_cb *cb, void *baton, long *preq_id); + +apr_status_t md_http_await(md_http_t *http, long req_id); + +void md_http_req_destroy(md_http_request_t *req); + +/**************************************************************************************************/ +/* interface to implementation */ + +typedef apr_status_t md_http_init_cb(void); +typedef void md_http_req_cleanup_cb(md_http_request_t *req); +typedef apr_status_t md_http_perform_cb(md_http_request_t *req); + +typedef struct md_http_impl_t md_http_impl_t; +struct md_http_impl_t { + md_http_init_cb *init; + md_http_req_cleanup_cb *req_cleanup; + md_http_perform_cb *perform; +}; + +void md_http_use_implementation(md_http_impl_t *impl); + + + +#endif /* md_http_h */ diff --git a/modules/md/md_json.c b/modules/md/md_json.c new file mode 100644 index 0000000..f73ab14 --- /dev/null +++ b/modules/md/md_json.c @@ -0,0 +1,1037 @@ +/* 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. + */ + +#include <assert.h> +#include <apr_lib.h> +#include <apr_strings.h> +#include <apr_buckets.h> + +#include "md_json.h" +#include "md_log.h" +#include "md_http.h" +#include "md_util.h" + +/* jansson thinks everyone compiles with the platform's cc in its fullest capabilities + * when undefining their INLINEs, we get static, unused functions, arg + */ +#if defined(__GNUC__) +#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) +#pragma GCC diagnostic push +#endif +#pragma GCC diagnostic ignored "-Wunused-function" +#pragma GCC diagnostic ignored "-Wunreachable-code" +#elif defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" +#endif + +#include <jansson_config.h> +#undef JSON_INLINE +#define JSON_INLINE +#include <jansson.h> + +#if defined(__GNUC__) +#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) +#pragma GCC diagnostic pop +#endif +#elif defined(__clang__) +#pragma clang diagnostic pop +#endif + +struct md_json_t { + apr_pool_t *p; + json_t *j; +}; + +/**************************************************************************************************/ +/* lifecycle */ + +static apr_status_t json_pool_cleanup(void *data) +{ + md_json_t *json = data; + if (json) { + md_json_destroy(json); + } + return APR_SUCCESS; +} + +static md_json_t *json_create(apr_pool_t *pool, json_t *j) +{ + md_json_t *json; + + if (!j) { + apr_abortfunc_t abfn = apr_pool_abort_get(pool); + if (abfn) { + abfn(APR_ENOMEM); + } + assert(j != NULL); /* failsafe in case abort is unset */ + } + json = apr_pcalloc(pool, sizeof(*json)); + json->p = pool; + json->j = j; + apr_pool_cleanup_register(pool, json, json_pool_cleanup, apr_pool_cleanup_null); + + return json; +} + +md_json_t *md_json_create(apr_pool_t *pool) +{ + return json_create(pool, json_object()); +} + +md_json_t *md_json_create_s(apr_pool_t *pool, const char *s) +{ + return json_create(pool, json_string(s)); +} + +void md_json_destroy(md_json_t *json) +{ + if (json && json->j) { + assert(json->j->refcount > 0); + json_decref(json->j); + json->j = NULL; + } +} + +md_json_t *md_json_copy(apr_pool_t *pool, md_json_t *json) +{ + return json_create(pool, json_copy(json->j)); +} + +md_json_t *md_json_clone(apr_pool_t *pool, md_json_t *json) +{ + return json_create(pool, json_deep_copy(json->j)); +} + +/**************************************************************************************************/ +/* selectors */ + + +static json_t *jselect(md_json_t *json, va_list ap) +{ + json_t *j; + const char *key; + + j = json->j; + key = va_arg(ap, char *); + while (key && j) { + j = json_object_get(j, key); + key = va_arg(ap, char *); + } + return j; +} + +static json_t *jselect_parent(const char **child_key, int create, md_json_t *json, va_list ap) +{ + const char *key, *next; + json_t *j, *jn; + + *child_key = NULL; + j = json->j; + key = va_arg(ap, char *); + while (key && j) { + next = va_arg(ap, char *); + if (next) { + jn = json_object_get(j, key); + if (!jn && create) { + jn = json_object(); + json_object_set_new(j, key, jn); + } + j = jn; + } + else { + *child_key = key; + } + key = next; + } + return j; +} + +static apr_status_t jselect_add(json_t *val, md_json_t *json, va_list ap) +{ + const char *key; + json_t *j, *aj; + + j = jselect_parent(&key, 1, json, ap); + + if (!j || !json_is_object(j)) { + json_decref(val); + return APR_EINVAL; + } + + aj = json_object_get(j, key); + if (!aj) { + aj = json_array(); + json_object_set_new(j, key, aj); + } + + if (!json_is_array(aj)) { + json_decref(val); + return APR_EINVAL; + } + + json_array_append(aj, val); + return APR_SUCCESS; +} + +static apr_status_t jselect_set(json_t *val, md_json_t *json, va_list ap) +{ + const char *key; + json_t *j; + + j = jselect_parent(&key, 1, json, ap); + + if (!j) { + json_decref(val); + return APR_EINVAL; + } + + if (key) { + if (!json_is_object(j)) { + json_decref(val); + return APR_EINVAL; + } + json_object_set(j, key, val); + } + else { + /* replace */ + if (json->j) { + json_decref(json->j); + } + json_incref(val); + json->j = val; + } + return APR_SUCCESS; +} + +static apr_status_t jselect_set_new(json_t *val, md_json_t *json, va_list ap) +{ + const char *key; + json_t *j; + + j = jselect_parent(&key, 1, json, ap); + + if (!j) { + json_decref(val); + return APR_EINVAL; + } + + if (key) { + if (!json_is_object(j)) { + json_decref(val); + return APR_EINVAL; + } + json_object_set_new(j, key, val); + } + else { + /* replace */ + if (json->j) { + json_decref(json->j); + } + json->j = val; + } + return APR_SUCCESS; +} + +int md_json_has_key(md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + return j != NULL; +} + +/**************************************************************************************************/ +/* booleans */ + +int md_json_getb(md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + return j? json_is_true(j) : 0; +} + +apr_status_t md_json_setb(int value, md_json_t *json, ...) +{ + va_list ap; + apr_status_t rv; + + va_start(ap, json); + rv = jselect_set_new(json_boolean(value), json, ap); + va_end(ap); + return rv; +} + +/**************************************************************************************************/ +/* numbers */ + +double md_json_getn(md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + return (j && json_is_number(j))? json_number_value(j) : 0.0; +} + +apr_status_t md_json_setn(double value, md_json_t *json, ...) +{ + va_list ap; + apr_status_t rv; + + va_start(ap, json); + rv = jselect_set_new(json_real(value), json, ap); + va_end(ap); + return rv; +} + +/**************************************************************************************************/ +/* longs */ + +long md_json_getl(md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + return (long)((j && json_is_number(j))? json_integer_value(j) : 0L); +} + +apr_status_t md_json_setl(long value, md_json_t *json, ...) +{ + va_list ap; + apr_status_t rv; + + va_start(ap, json); + rv = jselect_set_new(json_integer(value), json, ap); + va_end(ap); + return rv; +} + +/**************************************************************************************************/ +/* strings */ + +const char *md_json_gets(md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + return (j && json_is_string(j))? json_string_value(j) : NULL; +} + +const char *md_json_dups(apr_pool_t *p, md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + return (j && json_is_string(j))? apr_pstrdup(p, json_string_value(j)) : NULL; +} + +apr_status_t md_json_sets(const char *value, md_json_t *json, ...) +{ + va_list ap; + apr_status_t rv; + + va_start(ap, json); + rv = jselect_set_new(json_string(value), json, ap); + va_end(ap); + return rv; +} + +/**************************************************************************************************/ +/* json itself */ + +md_json_t *md_json_getj(md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (j) { + if (j == json->j) { + return json; + } + json_incref(j); + return json_create(json->p, j); + } + return NULL; +} + +apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...) +{ + va_list ap; + apr_status_t rv; + const char *key; + json_t *j; + + if (value) { + va_start(ap, json); + rv = jselect_set(value->j, json, ap); + va_end(ap); + } + else { + va_start(ap, json); + j = jselect_parent(&key, 1, json, ap); + va_end(ap); + + if (key && j && !json_is_object(j)) { + json_object_del(j, key); + rv = APR_SUCCESS; + } + else { + rv = APR_EINVAL; + } + } + return rv; +} + +apr_status_t md_json_addj(md_json_t *value, md_json_t *json, ...) +{ + va_list ap; + apr_status_t rv; + + va_start(ap, json); + rv = jselect_add(value->j, json, ap); + va_end(ap); + return rv; +} + + +/**************************************************************************************************/ +/* arrays / objects */ + +apr_status_t md_json_clr(md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (j && json_is_object(j)) { + json_object_clear(j); + } + else if (j && json_is_array(j)) { + json_array_clear(j); + } + return APR_SUCCESS; +} + +apr_status_t md_json_del(md_json_t *json, ...) +{ + const char *key; + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect_parent(&key, 0, json, ap); + va_end(ap); + + if (key && j && json_is_object(j)) { + json_object_del(j, key); + } + return APR_SUCCESS; +} + +/**************************************************************************************************/ +/* object strings */ + +apr_status_t md_json_gets_dict(apr_table_t *dict, md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (j && json_is_object(j)) { + const char *key; + json_t *val; + + json_object_foreach(j, key, val) { + if (json_is_string(val)) { + apr_table_set(dict, key, json_string_value(val)); + } + } + return APR_SUCCESS; + } + return APR_ENOENT; +} + +static int object_set(void *data, const char *key, const char *val) +{ + json_t *j = data, *nj = json_string(val); + json_object_set(j, key, nj); + json_decref(nj); + return 1; +} + +apr_status_t md_json_sets_dict(apr_table_t *dict, md_json_t *json, ...) +{ + json_t *nj, *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (!j || !json_is_object(j)) { + const char *key; + + va_start(ap, json); + j = jselect_parent(&key, 1, json, ap); + va_end(ap); + + if (!key || !j || !json_is_object(j)) { + return APR_EINVAL; + } + nj = json_object(); + json_object_set_new(j, key, nj); + j = nj; + } + + apr_table_do(object_set, j, dict, NULL); + return APR_SUCCESS; +} + +/**************************************************************************************************/ +/* conversions */ + +apr_status_t md_json_pass_to(void *value, md_json_t *json, apr_pool_t *p, void *baton) +{ + (void)p; + (void)baton; + return md_json_setj(value, json, NULL); +} + +apr_status_t md_json_pass_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton) +{ + (void)p; + (void)baton; + *pvalue = json; + return APR_SUCCESS; +} + +apr_status_t md_json_clone_to(void *value, md_json_t *json, apr_pool_t *p, void *baton) +{ + (void)baton; + return md_json_setj(md_json_clone(p, value), json, NULL); +} + +apr_status_t md_json_clone_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton) +{ + (void)baton; + *pvalue = md_json_clone(p, json); + return APR_SUCCESS; +} + +/**************************************************************************************************/ +/* array generic */ + +apr_status_t md_json_geta(apr_array_header_t *a, md_json_from_cb *cb, void *baton, + md_json_t *json, ...) +{ + json_t *j; + va_list ap; + apr_status_t rv = APR_SUCCESS; + size_t index; + json_t *val; + md_json_t wrap; + void *element; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (!j || !json_is_array(j)) { + return APR_ENOENT; + } + + wrap.p = a->pool; + json_array_foreach(j, index, val) { + wrap.j = val; + if (APR_SUCCESS == (rv = cb(&element, &wrap, wrap.p, baton))) { + if (element) { + APR_ARRAY_PUSH(a, void*) = element; + } + } + else if (APR_ENOENT == rv) { + rv = APR_SUCCESS; + } + else { + break; + } + } + return rv; +} + +apr_status_t md_json_seta(apr_array_header_t *a, md_json_to_cb *cb, void *baton, + md_json_t *json, ...) +{ + json_t *j, *nj; + md_json_t wrap; + apr_status_t rv = APR_SUCCESS; + va_list ap; + int i; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (!j || !json_is_array(j)) { + const char *key; + + va_start(ap, json); + j = jselect_parent(&key, 1, json, ap); + va_end(ap); + + if (!key || !j || !json_is_object(j)) { + return APR_EINVAL; + } + nj = json_array(); + json_object_set_new(j, key, nj); + j = nj; + } + + json_array_clear(j); + wrap.p = json->p; + for (i = 0; i < a->nelts; ++i) { + if (!cb) { + return APR_EINVAL; + } + wrap.j = json_string(""); + if (APR_SUCCESS == (rv = cb(APR_ARRAY_IDX(a, i, void*), &wrap, json->p, baton))) { + json_array_append_new(j, wrap.j); + } + } + return rv; +} + +int md_json_itera(md_json_itera_cb *cb, void *baton, md_json_t *json, ...) +{ + json_t *j; + va_list ap; + size_t index; + json_t *val; + md_json_t wrap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (!j || !json_is_array(j)) { + return 0; + } + + wrap.p = json->p; + json_array_foreach(j, index, val) { + wrap.j = val; + if (!cb(baton, index, &wrap)) { + return 0; + } + } + return 1; +} + +/**************************************************************************************************/ +/* array strings */ + +apr_status_t md_json_getsa(apr_array_header_t *a, md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (j && json_is_array(j)) { + size_t index; + json_t *val; + + json_array_foreach(j, index, val) { + if (json_is_string(val)) { + APR_ARRAY_PUSH(a, const char *) = json_string_value(val); + } + } + return APR_SUCCESS; + } + return APR_ENOENT; +} + +apr_status_t md_json_dupsa(apr_array_header_t *a, apr_pool_t *p, md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (j && json_is_array(j)) { + size_t index; + json_t *val; + + json_array_foreach(j, index, val) { + if (json_is_string(val)) { + APR_ARRAY_PUSH(a, const char *) = apr_pstrdup(p, json_string_value(val)); + } + } + return APR_SUCCESS; + } + return APR_ENOENT; +} + +apr_status_t md_json_setsa(apr_array_header_t *a, md_json_t *json, ...) +{ + json_t *nj, *j; + va_list ap; + int i; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (!j || !json_is_array(j)) { + const char *key; + + va_start(ap, json); + j = jselect_parent(&key, 1, json, ap); + va_end(ap); + + if (!key || !j || !json_is_object(j)) { + return APR_EINVAL; + } + nj = json_array(); + json_object_set_new(j, key, nj); + j = nj; + } + + json_array_clear(j); + for (i = 0; i < a->nelts; ++i) { + json_array_append_new(j, json_string(APR_ARRAY_IDX(a, i, const char*))); + } + return APR_SUCCESS; +} + +/**************************************************************************************************/ +/* formatting, parsing */ + +typedef struct { + md_json_t *json; + md_json_fmt_t fmt; + const char *fname; + apr_file_t *f; +} j_write_ctx; + +/* Convert from md_json_fmt_t to the Jansson json_dumpX flags. */ +static size_t fmt_to_flags(md_json_fmt_t fmt) +{ + /* NOTE: JSON_PRESERVE_ORDER is off by default before Jansson 2.8. It + * doesn't have any semantic effect on the protocol, but it does let the + * md_json_writeX unit tests run deterministically. */ + return JSON_PRESERVE_ORDER | + ((fmt == MD_JSON_FMT_COMPACT) ? JSON_COMPACT : JSON_INDENT(2)); +} + +static int dump_cb(const char *buffer, size_t len, void *baton) +{ + apr_bucket_brigade *bb = baton; + apr_status_t rv; + + rv = apr_brigade_write(bb, NULL, NULL, buffer, len); + return (rv == APR_SUCCESS)? 0 : -1; +} + +apr_status_t md_json_writeb(md_json_t *json, md_json_fmt_t fmt, apr_bucket_brigade *bb) +{ + int rv = json_dump_callback(json->j, dump_cb, bb, fmt_to_flags(fmt)); + return rv? APR_EGENERAL : APR_SUCCESS; +} + +static int chunk_cb(const char *buffer, size_t len, void *baton) +{ + apr_array_header_t *chunks = baton; + char *chunk = apr_pcalloc(chunks->pool, len+1); + + memcpy(chunk, buffer, len); + APR_ARRAY_PUSH(chunks, const char *) = chunk; + return 0; +} + +const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt) +{ + apr_array_header_t *chunks; + int rv; + + chunks = apr_array_make(p, 10, sizeof(char *)); + rv = json_dump_callback(json->j, chunk_cb, chunks, fmt_to_flags(fmt)); + + if (rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "md_json_writep failed to dump JSON"); + return NULL; + } + + switch (chunks->nelts) { + case 0: + return ""; + case 1: + return APR_ARRAY_IDX(chunks, 0, const char *); + default: + return apr_array_pstrcat(p, chunks, 0); + } +} + +apr_status_t md_json_writef(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, apr_file_t *f) +{ + apr_status_t rv; + const char *s; + + s = md_json_writep(json, p, fmt); + + if (s) { + rv = apr_file_write_full(f, s, strlen(s), NULL); + } + else { + rv = APR_EINVAL; + } + + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, json->p, "md_json_writef"); + } + return rv; +} + +apr_status_t md_json_fcreatex(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, + const char *fpath, apr_fileperms_t perms) +{ + apr_status_t rv; + apr_file_t *f; + + rv = md_util_fcreatex(&f, fpath, perms, p); + if (APR_SUCCESS == rv) { + rv = md_json_writef(json, p, fmt, f); + apr_file_close(f); + } + return rv; +} + +static apr_status_t write_json(void *baton, apr_file_t *f, apr_pool_t *p) +{ + j_write_ctx *ctx = baton; + apr_status_t rv = md_json_writef(ctx->json, p, ctx->fmt, f); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "freplace json in %s", ctx->fname); + } + return rv; +} + +apr_status_t md_json_freplace(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, + const char *fpath, apr_fileperms_t perms) +{ + j_write_ctx ctx; + ctx.json = json; + ctx.fmt = fmt; + ctx.fname = fpath; + return md_util_freplace(fpath, perms, p, write_json, &ctx); +} + +apr_status_t md_json_readd(md_json_t **pjson, apr_pool_t *pool, const char *data, size_t data_len) +{ + json_error_t error; + json_t *j; + + j = json_loadb(data, data_len, 0, &error); + if (!j) { + return APR_EINVAL; + } + *pjson = json_create(pool, j); + return APR_SUCCESS; +} + +static size_t load_cb(void *data, size_t max_len, void *baton) +{ + apr_bucket_brigade *body = baton; + size_t blen, read_len = 0; + const char *bdata; + char *dest = data; + apr_bucket *b; + apr_status_t rv; + + while (body && !APR_BRIGADE_EMPTY(body) && max_len > 0) { + b = APR_BRIGADE_FIRST(body); + if (APR_BUCKET_IS_METADATA(b)) { + if (APR_BUCKET_IS_EOS(b)) { + body = NULL; + } + } + else { + rv = apr_bucket_read(b, &bdata, &blen, APR_BLOCK_READ); + if (rv == APR_SUCCESS) { + if (blen > max_len) { + apr_bucket_split(b, max_len); + blen = max_len; + } + memcpy(dest, bdata, blen); + read_len += blen; + max_len -= blen; + dest += blen; + } + else { + body = NULL; + if (!APR_STATUS_IS_EOF(rv)) { + /* everything beside EOF is an error */ + read_len = (size_t)-1; + } + } + } + APR_BUCKET_REMOVE(b); + apr_bucket_delete(b); + } + + return read_len; +} + +apr_status_t md_json_readb(md_json_t **pjson, apr_pool_t *pool, apr_bucket_brigade *bb) +{ + json_error_t error; + json_t *j; + + j = json_load_callback(load_cb, bb, 0, &error); + if (!j) { + return APR_EINVAL; + } + *pjson = json_create(pool, j); + return APR_SUCCESS; +} + +static size_t load_file_cb(void *data, size_t max_len, void *baton) +{ + apr_file_t *f = baton; + apr_size_t len = max_len; + apr_status_t rv; + + rv = apr_file_read(f, data, &len); + if (APR_SUCCESS == rv) { + return len; + } + else if (APR_EOF == rv) { + return 0; + } + return (size_t)-1; +} + +apr_status_t md_json_readf(md_json_t **pjson, apr_pool_t *p, const char *fpath) +{ + apr_file_t *f; + json_t *j; + apr_status_t rv; + json_error_t error; + + rv = apr_file_open(&f, fpath, APR_FOPEN_READ, 0, p); + if (rv != APR_SUCCESS) { + return rv; + } + + j = json_load_callback(load_file_cb, f, 0, &error); + if (j) { + *pjson = json_create(p, j); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "failed to load JSON file %s: %s (line %d:%d)", + fpath, error.text, error.line, error.column); + } + + apr_file_close(f); + return (j && *pjson) ? APR_SUCCESS : APR_EINVAL; +} + +/**************************************************************************************************/ +/* http get */ + +apr_status_t md_json_read_http(md_json_t **pjson, apr_pool_t *pool, const md_http_response_t *res) +{ + apr_status_t rv = APR_ENOENT; + if (res->rv == APR_SUCCESS) { + const char *ctype = apr_table_get(res->headers, "content-type"); + if (ctype && res->body && (strstr(ctype, "/json") || strstr(ctype, "+json"))) { + rv = md_json_readb(pjson, pool, res->body); + } + } + return rv; +} + +typedef struct { + apr_status_t rv; + apr_pool_t *pool; + md_json_t *json; +} resp_data; + +static apr_status_t json_resp_cb(const md_http_response_t *res) +{ + resp_data *resp = res->req->baton; + return md_json_read_http(&resp->json, resp->pool, res); +} + +apr_status_t md_json_http_get(md_json_t **pjson, apr_pool_t *pool, + struct md_http_t *http, const char *url) +{ + long req_id; + apr_status_t rv; + resp_data resp; + + memset(&resp, 0, sizeof(resp)); + resp.pool = pool; + + rv = md_http_GET(http, url, NULL, json_resp_cb, &resp, &req_id); + + if (rv == APR_SUCCESS) { + md_http_await(http, req_id); + *pjson = resp.json; + return resp.rv; + } + *pjson = NULL; + return rv; +} + diff --git a/modules/md/md_json.h b/modules/md/md_json.h new file mode 100644 index 0000000..7f2e4f3 --- /dev/null +++ b/modules/md/md_json.h @@ -0,0 +1,122 @@ +/* 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. + */ + +#ifndef mod_md_md_json_h +#define mod_md_md_json_h + +#include <apr_file_io.h> + +struct apr_bucket_brigade; +struct apr_file_t; + +struct md_http_t; +struct md_http_response_t; + + +typedef struct md_json_t md_json_t; + +typedef enum { + MD_JSON_FMT_COMPACT, + MD_JSON_FMT_INDENT, +} md_json_fmt_t; + +md_json_t *md_json_create(apr_pool_t *pool); +void md_json_destroy(md_json_t *json); + +md_json_t *md_json_copy(apr_pool_t *pool, md_json_t *json); +md_json_t *md_json_clone(apr_pool_t *pool, md_json_t *json); + +int md_json_has_key(md_json_t *json, ...); + +/* boolean manipulation */ +int md_json_getb(md_json_t *json, ...); +apr_status_t md_json_setb(int value, md_json_t *json, ...); + +/* number manipulation */ +double md_json_getn(md_json_t *json, ...); +apr_status_t md_json_setn(double value, md_json_t *json, ...); + +/* long manipulation */ +long md_json_getl(md_json_t *json, ...); +apr_status_t md_json_setl(long value, md_json_t *json, ...); + +/* string manipulation */ +md_json_t *md_json_create_s(apr_pool_t *pool, const char *s); +const char *md_json_gets(md_json_t *json, ...); +const char *md_json_dups(apr_pool_t *p, md_json_t *json, ...); +apr_status_t md_json_sets(const char *s, md_json_t *json, ...); + +/* json manipulation */ +md_json_t *md_json_getj(md_json_t *json, ...); +apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...); +apr_status_t md_json_addj(md_json_t *value, md_json_t *json, ...); + +/* Array/Object manipulation */ +apr_status_t md_json_clr(md_json_t *json, ...); +apr_status_t md_json_del(md_json_t *json, ...); + +/* conversion function from and to json */ +typedef apr_status_t md_json_to_cb(void *value, md_json_t *json, apr_pool_t *p, void *baton); +typedef apr_status_t md_json_from_cb(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton); + +/* identity pass through from json to json */ +apr_status_t md_json_pass_to(void *value, md_json_t *json, apr_pool_t *p, void *baton); +apr_status_t md_json_pass_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton); + +/* conversions from json to json in specified pool */ +apr_status_t md_json_clone_to(void *value, md_json_t *json, apr_pool_t *p, void *baton); +apr_status_t md_json_clone_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton); + +/* Manipulating/Iteration on generic Arrays */ +apr_status_t md_json_geta(apr_array_header_t *a, md_json_from_cb *cb, + void *baton, md_json_t *json, ...); +apr_status_t md_json_seta(apr_array_header_t *a, md_json_to_cb *cb, + void *baton, md_json_t *json, ...); + +typedef int md_json_itera_cb(void *baton, size_t index, md_json_t *json); +int md_json_itera(md_json_itera_cb *cb, void *baton, md_json_t *json, ...); + +/* Manipulating Object String values */ +apr_status_t md_json_gets_dict(apr_table_t *dict, md_json_t *json, ...); +apr_status_t md_json_sets_dict(apr_table_t *dict, md_json_t *json, ...); + +/* Manipulating String Arrays */ +apr_status_t md_json_getsa(apr_array_header_t *a, md_json_t *json, ...); +apr_status_t md_json_dupsa(apr_array_header_t *a, apr_pool_t *p, md_json_t *json, ...); +apr_status_t md_json_setsa(apr_array_header_t *a, md_json_t *json, ...); + +/* serialization & parsing */ +apr_status_t md_json_writeb(md_json_t *json, md_json_fmt_t fmt, struct apr_bucket_brigade *bb); +const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt); +apr_status_t md_json_writef(md_json_t *json, apr_pool_t *p, + md_json_fmt_t fmt, struct apr_file_t *f); +apr_status_t md_json_fcreatex(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, + const char *fpath, apr_fileperms_t perms); +apr_status_t md_json_freplace(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, + const char *fpath, apr_fileperms_t perms); + +apr_status_t md_json_readb(md_json_t **pjson, apr_pool_t *pool, struct apr_bucket_brigade *bb); +apr_status_t md_json_readd(md_json_t **pjson, apr_pool_t *pool, const char *data, size_t data_len); +apr_status_t md_json_readf(md_json_t **pjson, apr_pool_t *pool, const char *fpath); + + +/* http retrieval */ +apr_status_t md_json_http_get(md_json_t **pjson, apr_pool_t *pool, + struct md_http_t *http, const char *url); +apr_status_t md_json_read_http(md_json_t **pjson, apr_pool_t *pool, + const struct md_http_response_t *res); + +#endif /* md_json_h */ diff --git a/modules/md/md_jws.c b/modules/md/md_jws.c new file mode 100644 index 0000000..37c1b0e --- /dev/null +++ b/modules/md/md_jws.c @@ -0,0 +1,106 @@ +/* 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. + */ + +#include <apr_lib.h> +#include <apr_strings.h> +#include <apr_tables.h> +#include <apr_buckets.h> + +#include "md_crypt.h" +#include "md_json.h" +#include "md_jws.h" +#include "md_log.h" +#include "md_util.h" + +static int header_set(void *data, const char *key, const char *val) +{ + md_json_sets(val, (md_json_t *)data, key, NULL); + return 1; +} + +apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p, + const char *payload, size_t len, + struct apr_table_t *protected, + struct md_pkey_t *pkey, const char *key_id) +{ + md_json_t *msg, *jprotected; + const char *prot64, *pay64, *sign64, *sign, *prot; + apr_status_t rv = APR_SUCCESS; + + *pmsg = NULL; + + msg = md_json_create(p); + + jprotected = md_json_create(p); + md_json_sets("RS256", jprotected, "alg", NULL); + if (key_id) { + md_json_sets(key_id, jprotected, "kid", NULL); + } + else { + md_json_sets(md_pkey_get_rsa_e64(pkey, p), jprotected, "jwk", "e", NULL); + md_json_sets("RSA", jprotected, "jwk", "kty", NULL); + md_json_sets(md_pkey_get_rsa_n64(pkey, p), jprotected, "jwk", "n", NULL); + } + apr_table_do(header_set, jprotected, protected, NULL); + prot = md_json_writep(jprotected, p, MD_JSON_FMT_COMPACT); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, p, "protected: %s", + prot ? prot : "<failed to serialize!>"); + + if (!prot) { + rv = APR_EINVAL; + } + + if (rv == APR_SUCCESS) { + prot64 = md_util_base64url_encode(prot, strlen(prot), p); + md_json_sets(prot64, msg, "protected", NULL); + pay64 = md_util_base64url_encode(payload, len, p); + + md_json_sets(pay64, msg, "payload", NULL); + sign = apr_psprintf(p, "%s.%s", prot64, pay64); + + rv = md_crypt_sign64(&sign64, pkey, p, sign, strlen(sign)); + } + + if (rv == APR_SUCCESS) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, + "jws pay64=%s\nprot64=%s\nsign64=%s", pay64, prot64, sign64); + + md_json_sets(sign64, msg, "signature", NULL); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "jwk signed message"); + } + + *pmsg = (APR_SUCCESS == rv)? msg : NULL; + return rv; +} + +apr_status_t md_jws_pkey_thumb(const char **pthumb, apr_pool_t *p, struct md_pkey_t *pkey) +{ + const char *e64, *n64, *s; + apr_status_t rv; + + e64 = md_pkey_get_rsa_e64(pkey, p); + n64 = md_pkey_get_rsa_n64(pkey, p); + if (!e64 || !n64) { + return APR_EINVAL; + } + + /* whitespace and order is relevant, since we hand out a digest of this */ + s = apr_psprintf(p, "{\"e\":\"%s\",\"kty\":\"RSA\",\"n\":\"%s\"}", e64, n64); + rv = md_crypt_sha256_digest64(pthumb, p, s, strlen(s)); + return rv; +} diff --git a/modules/md/md_jws.h b/modules/md/md_jws.h new file mode 100644 index 0000000..e7c145e --- /dev/null +++ b/modules/md/md_jws.h @@ -0,0 +1,30 @@ +/* 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. + */ + +#ifndef mod_md_md_jws_h +#define mod_md_md_jws_h + +struct apr_table_t; +struct md_json_t; +struct md_pkey_t; + +apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p, + const char *payload, size_t len, struct apr_table_t *protected, + struct md_pkey_t *pkey, const char *key_id); + +apr_status_t md_jws_pkey_thumb(const char **pthumb, apr_pool_t *p, struct md_pkey_t *pkey); + +#endif /* md_jws_h */ diff --git a/modules/md/md_log.c b/modules/md/md_log.c new file mode 100644 index 0000000..d236e0f --- /dev/null +++ b/modules/md/md_log.c @@ -0,0 +1,78 @@ +/* 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. + */ + +#include <apr_lib.h> +#include <apr_strings.h> +#include <apr_buckets.h> + +#include "md_log.h" + +#define LOG_BUFFER_LEN 1024 + +static const char *level_names[] = { + "emergency", + "alert", + "crit", + "err", + "warning", + "notice", + "info", + "debug", + "trace1", + "trace2", + "trace3", + "trace4", + "trace5", + "trace6", + "trace7", + "trace8", +}; + +const char *md_log_level_name(md_log_level_t level) +{ + return level_names[level]; +} + +static md_log_print_cb *log_printv; +static md_log_level_cb *log_level; +static void *log_baton; + +void md_log_set(md_log_level_cb *level_cb, md_log_print_cb *print_cb, void *baton) +{ + log_printv = print_cb; + log_level = level_cb; + log_baton = baton; +} + +int md_log_is_level(apr_pool_t *p, md_log_level_t level) +{ + if (!log_level) { + return 0; + } + return log_level(log_baton, p, level); +} + +void md_log_perror(const char *file, int line, md_log_level_t level, + apr_status_t rv, apr_pool_t *p, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + if (log_printv) { + log_printv(file, line, level, rv, log_baton, p, fmt, ap); + } + va_end(ap); +} diff --git a/modules/md/md_log.h b/modules/md/md_log.h new file mode 100644 index 0000000..73885f2 --- /dev/null +++ b/modules/md/md_log.h @@ -0,0 +1,56 @@ +/* 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. + */ + +#ifndef mod_md_md_log_h +#define mod_md_md_log_h + +typedef enum { + MD_LOG_EMERG, + MD_LOG_ALERT, + MD_LOG_CRIT, + MD_LOG_ERR, + MD_LOG_WARNING, + MD_LOG_NOTICE, + MD_LOG_INFO, + MD_LOG_DEBUG, + MD_LOG_TRACE1, + MD_LOG_TRACE2, + MD_LOG_TRACE3, + MD_LOG_TRACE4, + MD_LOG_TRACE5, + MD_LOG_TRACE6, + MD_LOG_TRACE7, + MD_LOG_TRACE8, +} md_log_level_t; + +#define MD_LOG_MARK __FILE__,__LINE__ + +const char *md_log_level_name(md_log_level_t level); + +int md_log_is_level(apr_pool_t *p, md_log_level_t level); + +void md_log_perror(const char *file, int line, md_log_level_t level, + apr_status_t rv, apr_pool_t *p, const char *fmt, ...) + __attribute__((format(printf,6,7))); + +typedef int md_log_level_cb(void *baton, apr_pool_t *p, md_log_level_t level); + +typedef void md_log_print_cb(const char *file, int line, md_log_level_t level, + apr_status_t rv, void *baton, apr_pool_t *p, const char *fmt, va_list ap); + +void md_log_set(md_log_level_cb *level_cb, md_log_print_cb *print_cb, void *baton); + +#endif /* md_log_h */ diff --git a/modules/md/md_reg.c b/modules/md/md_reg.c new file mode 100644 index 0000000..233fea7 --- /dev/null +++ b/modules/md/md_reg.c @@ -0,0 +1,1004 @@ +/* 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. + */ + +#include <assert.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> + +#include <apr_lib.h> +#include <apr_hash.h> +#include <apr_strings.h> +#include <apr_uri.h> + +#include "md.h" +#include "md_crypt.h" +#include "md_log.h" +#include "md_json.h" +#include "md_reg.h" +#include "md_store.h" +#include "md_util.h" + +#include "md_acme.h" +#include "md_acme_acct.h" + +struct md_reg_t { + struct md_store_t *store; + struct apr_hash_t *protos; + int can_http; + int can_https; + const char *proxy_url; +}; + +/**************************************************************************************************/ +/* life cycle */ + +static apr_status_t load_props(md_reg_t *reg, apr_pool_t *p) +{ + md_json_t *json; + apr_status_t rv; + + rv = md_store_load(reg->store, MD_SG_NONE, NULL, MD_FN_HTTPD_JSON, + MD_SV_JSON, (void**)&json, p); + if (APR_SUCCESS == rv) { + if (md_json_has_key(json, MD_KEY_PROTO, MD_KEY_HTTP, NULL)) { + reg->can_http = md_json_getb(json, MD_KEY_PROTO, MD_KEY_HTTP, NULL); + } + if (md_json_has_key(json, MD_KEY_PROTO, MD_KEY_HTTPS, NULL)) { + reg->can_https = md_json_getb(json, MD_KEY_PROTO, MD_KEY_HTTPS, NULL); + } + } + else if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + } + return rv; +} + +apr_status_t md_reg_init(md_reg_t **preg, apr_pool_t *p, struct md_store_t *store, + const char *proxy_url) +{ + md_reg_t *reg; + apr_status_t rv; + + reg = apr_pcalloc(p, sizeof(*reg)); + reg->store = store; + reg->protos = apr_hash_make(p); + reg->can_http = 1; + reg->can_https = 1; + reg->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL; + + if (APR_SUCCESS == (rv = md_acme_protos_add(reg->protos, p))) { + rv = load_props(reg, p); + } + + *preg = (rv == APR_SUCCESS)? reg : NULL; + return rv; +} + +struct md_store_t *md_reg_store_get(md_reg_t *reg) +{ + return reg->store; +} + +/**************************************************************************************************/ +/* checks */ + +static apr_status_t check_values(md_reg_t *reg, apr_pool_t *p, const md_t *md, int fields) +{ + apr_status_t rv = APR_SUCCESS; + const char *err = NULL; + + if (MD_UPD_DOMAINS & fields) { + const md_t *other; + const char *domain; + int i; + + if (!md->domains || md->domains->nelts <= 0) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, APR_EINVAL, p, + "empty domain list: %s", md->name); + return APR_EINVAL; + } + + for (i = 0; i < md->domains->nelts; ++i) { + domain = APR_ARRAY_IDX(md->domains, i, const char *); + if (!md_util_is_dns_name(p, domain, 1)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, + "md %s with invalid domain name: %s", md->name, domain); + return APR_EINVAL; + } + } + + if (NULL != (other = md_reg_find_overlap(reg, md, &domain, p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, + "md %s shares domain '%s' with md %s", + md->name, domain, other->name); + return APR_EINVAL; + } + } + + if (MD_UPD_CONTACTS & fields) { + const char *contact; + int i; + + for (i = 0; i < md->contacts->nelts && !err; ++i) { + contact = APR_ARRAY_IDX(md->contacts, i, const char *); + rv = md_util_abs_uri_check(p, contact, &err); + + if (err) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, + "contact for %s invalid (%s): %s", md->name, err, contact); + return APR_EINVAL; + } + } + } + + if ((MD_UPD_CA_URL & fields) && md->ca_url) { /* setting to empty is ok */ + rv = md_util_abs_uri_check(p, md->ca_url, &err); + if (err) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, + "CA url for %s invalid (%s): %s", md->name, err, md->ca_url); + return APR_EINVAL; + } + } + + if ((MD_UPD_CA_PROTO & fields) && md->ca_proto) { /* setting to empty is ok */ + /* Do we want to restrict this to "known" protocols? */ + } + + if ((MD_UPD_CA_ACCOUNT & fields) && md->ca_account) { /* setting to empty is ok */ + /* hmm, in case we know the protocol, some checks could be done */ + } + + if ((MD_UPD_AGREEMENT & fields) && md->ca_agreement) { /* setting to empty is ok */ + rv = md_util_abs_uri_check(p, md->ca_agreement, &err); + if (err) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, + "CA url for %s invalid (%s): %s", md->name, err, md->ca_agreement); + return APR_EINVAL; + } + } + + return rv; +} + +/**************************************************************************************************/ +/* state assessment */ + +static apr_status_t state_init(md_reg_t *reg, apr_pool_t *p, md_t *md, int save_changes) +{ + md_state_t state = MD_S_UNKNOWN; + const md_creds_t *creds; + const md_cert_t *cert; + apr_time_t expires = 0, valid_from = 0; + apr_status_t rv; + int i; + + if (APR_SUCCESS == (rv = md_reg_creds_get(&creds, reg, MD_SG_DOMAINS, md, p))) { + state = MD_S_INCOMPLETE; + if (!creds->privkey) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "md{%s}: incomplete, without private key", md->name); + } + else if (!creds->cert) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "md{%s}: incomplete, has key but no certificate", md->name); + } + else { + valid_from = md_cert_get_not_before(creds->cert); + expires = md_cert_get_not_after(creds->cert); + if (md_cert_has_expired(creds->cert)) { + state = MD_S_EXPIRED; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "md{%s}: expired, certificate has expired", md->name); + goto out; + } + if (!md_cert_is_valid_now(creds->cert)) { + state = MD_S_ERROR; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, + "md{%s}: error, certificate valid in future (clock wrong?)", + md->name); + goto out; + } + if (!md_cert_covers_md(creds->cert, md)) { + state = MD_S_INCOMPLETE; + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, + "md{%s}: incomplete, cert no longer covers all domains, " + "needs sign up for a new certificate", md->name); + goto out; + } + if (!md->must_staple != !md_cert_must_staple(creds->cert)) { + state = MD_S_INCOMPLETE; + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, + "md{%s}: OCSP Stapling is%s requested, but certificate " + "has it%s enabled. Need to get a new certificate.", md->name, + md->must_staple? "" : " not", + !md->must_staple? "" : " not"); + goto out; + } + + for (i = 1; i < creds->pubcert->nelts; ++i) { + cert = APR_ARRAY_IDX(creds->pubcert, i, const md_cert_t *); + if (!md_cert_is_valid_now(cert)) { + state = MD_S_ERROR; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, + "md{%s}: error, the certificate itself is valid, however the %d. " + "certificate in the chain is not valid now (clock wrong?).", + md->name, i); + goto out; + } + } + + state = MD_S_COMPLETE; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "md{%s}: is complete", md->name); + } + } + +out: + if (APR_SUCCESS != rv) { + state = MD_S_ERROR; + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "md{%s}: error", md->name); + } + + if (save_changes && md->state == state + && md->valid_from == valid_from && md->expires == expires) { + save_changes = 0; + } + md->state = state; + md->valid_from = valid_from; + md->expires = expires; + if (save_changes && APR_SUCCESS == rv) { + return md_save(reg->store, p, MD_SG_DOMAINS, md, 0); + } + return rv; +} + +apr_status_t md_reg_assess(md_reg_t *reg, md_t *md, int *perrored, int *prenew, apr_pool_t *p) +{ + int renew = 0; + int errored = 0; + + (void)reg; + switch (md->state) { + case MD_S_UNKNOWN: + md_log_perror( MD_LOG_MARK, MD_LOG_ERR, 0, p, "md(%s): in unknown state.", md->name); + break; + case MD_S_ERROR: + md_log_perror( MD_LOG_MARK, MD_LOG_ERR, 0, p, + "md(%s): in error state, unable to drive forward. If unable to " + " detect the cause, you may remove the staging or even domain " + " sub-directory for this MD and start all over.", md->name); + errored = 1; + break; + case MD_S_COMPLETE: + if (!md->expires) { + md_log_perror( MD_LOG_MARK, MD_LOG_WARNING, 0, p, + "md(%s): looks complete, but has unknown expiration date.", md->name); + errored = 1; + } + else if (md->expires <= apr_time_now()) { + /* Maybe we hibernated in the meantime? */ + md->state = MD_S_EXPIRED; + renew = 1; + } + else { + renew = md_should_renew(md); + } + break; + case MD_S_INCOMPLETE: + case MD_S_EXPIRED: + renew = 1; + break; + case MD_S_MISSING: + break; + } + *prenew = renew; + *perrored = errored; + return APR_SUCCESS; +} + +/**************************************************************************************************/ +/* iteration */ + +typedef struct { + md_reg_t *reg; + md_reg_do_cb *cb; + void *baton; + const char *exclude; + const void *result; +} reg_do_ctx; + +static int reg_md_iter(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp) +{ + reg_do_ctx *ctx = baton; + + (void)store; + if (!ctx->exclude || strcmp(ctx->exclude, md->name)) { + state_init(ctx->reg, ptemp, (md_t*)md, 1); + return ctx->cb(ctx->baton, ctx->reg, md); + } + return 1; +} + +static int reg_do(md_reg_do_cb *cb, void *baton, md_reg_t *reg, apr_pool_t *p, const char *exclude) +{ + reg_do_ctx ctx; + + ctx.reg = reg; + ctx.cb = cb; + ctx.baton = baton; + ctx.exclude = exclude; + return md_store_md_iter(reg_md_iter, &ctx, reg->store, p, MD_SG_DOMAINS, "*"); +} + + +int md_reg_do(md_reg_do_cb *cb, void *baton, md_reg_t *reg, apr_pool_t *p) +{ + return reg_do(cb, baton, reg, p, NULL); +} + +/**************************************************************************************************/ +/* lookup */ + +md_t *md_reg_get(md_reg_t *reg, const char *name, apr_pool_t *p) +{ + md_t *md; + + if (APR_SUCCESS == md_load(reg->store, MD_SG_DOMAINS, name, &md, p)) { + state_init(reg, p, md, 1); + return md; + } + return NULL; +} + +typedef struct { + const char *domain; + md_t *md; +} find_domain_ctx; + +static int find_domain(void *baton, md_reg_t *reg, md_t *md) +{ + find_domain_ctx *ctx = baton; + + (void)reg; + if (md_contains(md, ctx->domain, 0)) { + ctx->md = md; + return 0; + } + return 1; +} + +md_t *md_reg_find(md_reg_t *reg, const char *domain, apr_pool_t *p) +{ + find_domain_ctx ctx; + + ctx.domain = domain; + ctx.md = NULL; + + md_reg_do(find_domain, &ctx, reg, p); + if (ctx.md) { + state_init(reg, p, ctx.md, 1); + } + return ctx.md; +} + +typedef struct { + const md_t *md_checked; + md_t *md; + const char *s; +} find_overlap_ctx; + +static int find_overlap(void *baton, md_reg_t *reg, md_t *md) +{ + find_overlap_ctx *ctx = baton; + const char *overlap; + + (void)reg; + if ((overlap = md_common_name(ctx->md_checked, md))) { + ctx->md = md; + ctx->s = overlap; + return 0; + } + return 1; +} + +md_t *md_reg_find_overlap(md_reg_t *reg, const md_t *md, const char **pdomain, apr_pool_t *p) +{ + find_overlap_ctx ctx; + + ctx.md_checked = md; + ctx.md = NULL; + ctx.s = NULL; + + reg_do(find_overlap, &ctx, reg, p, md->name); + if (pdomain && ctx.s) { + *pdomain = ctx.s; + } + if (ctx.md) { + state_init(reg, p, ctx.md, 1); + } + return ctx.md; +} + +apr_status_t md_reg_get_cred_files(md_reg_t *reg, const md_t *md, apr_pool_t *p, + const char **pkeyfile, const char **pcertfile) +{ + apr_status_t rv; + + rv = md_store_get_fname(pkeyfile, reg->store, MD_SG_DOMAINS, md->name, MD_FN_PRIVKEY, p); + if (APR_SUCCESS == rv) { + rv = md_store_get_fname(pcertfile, reg->store, MD_SG_DOMAINS, md->name, MD_FN_PUBCERT, p); + } + return rv; +} + +/**************************************************************************************************/ +/* manipulation */ + +static apr_status_t p_md_add(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_reg_t *reg = baton; + apr_status_t rv = APR_SUCCESS; + md_t *md, *mine; + + md = va_arg(ap, md_t *); + mine = md_clone(ptemp, md); + if (APR_SUCCESS == (rv = check_values(reg, ptemp, md, MD_UPD_ALL)) + && APR_SUCCESS == (rv = state_init(reg, ptemp, mine, 0)) + && APR_SUCCESS == (rv = md_save(reg->store, p, MD_SG_DOMAINS, mine, 1))) { + } + return rv; +} + +apr_status_t md_reg_add(md_reg_t *reg, md_t *md, apr_pool_t *p) +{ + return md_util_pool_vdo(p_md_add, reg, p, md, NULL); +} + +static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_reg_t *reg = baton; + apr_status_t rv = APR_SUCCESS; + const char *name; + const md_t *md, *updates; + int fields; + md_t *nmd; + + name = va_arg(ap, const char *); + updates = va_arg(ap, const md_t *); + fields = va_arg(ap, int); + + if (NULL == (md = md_reg_get(reg, name, ptemp))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, APR_ENOENT, ptemp, "md %s", name); + return APR_ENOENT; + } + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "update md %s", name); + + if (APR_SUCCESS != (rv = check_values(reg, ptemp, updates, fields))) { + return rv; + } + + nmd = md_copy(ptemp, md); + if (MD_UPD_DOMAINS & fields) { + nmd->domains = updates->domains; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update domains: %s", name); + } + if (MD_UPD_CA_URL & fields) { + nmd->ca_url = updates->ca_url; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca url: %s", name); + } + if (MD_UPD_CA_PROTO & fields) { + nmd->ca_proto = updates->ca_proto; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca protocol: %s", name); + } + if (MD_UPD_CA_ACCOUNT & fields) { + nmd->ca_account = updates->ca_account; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update account: %s", name); + } + if (MD_UPD_CONTACTS & fields) { + nmd->contacts = updates->contacts; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update contacts: %s", name); + } + if (MD_UPD_AGREEMENT & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update agreement: %s", name); + nmd->ca_agreement = updates->ca_agreement; + } + if (MD_UPD_CERT_URL & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update cert url: %s", name); + nmd->cert_url = updates->cert_url; + } + if (MD_UPD_DRIVE_MODE & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update drive-mode: %s", name); + nmd->drive_mode = updates->drive_mode; + } + if (MD_UPD_RENEW_WINDOW & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update renew-window: %s", name); + nmd->renew_norm = updates->renew_norm; + nmd->renew_window = updates->renew_window; + } + if (MD_UPD_CA_CHALLENGES & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca challenges: %s", name); + nmd->ca_challenges = (updates->ca_challenges? + apr_array_copy(p, updates->ca_challenges) : NULL); + } + if (MD_UPD_PKEY_SPEC & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update pkey spec: %s", name); + nmd->pkey_spec = NULL; + if (updates->pkey_spec) { + nmd->pkey_spec = apr_pmemdup(p, updates->pkey_spec, sizeof(md_pkey_spec_t)); + } + } + if (MD_UPD_REQUIRE_HTTPS & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update require-https: %s", name); + nmd->require_https = updates->require_https; + } + if (MD_UPD_TRANSITIVE & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update transitive: %s", name); + nmd->transitive = updates->transitive; + } + if (MD_UPD_MUST_STAPLE & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update must-staple: %s", name); + nmd->must_staple = updates->must_staple; + } + + if (fields && APR_SUCCESS == (rv = md_save(reg->store, p, MD_SG_DOMAINS, nmd, 0))) { + rv = state_init(reg, ptemp, nmd, 0); + } + return rv; +} + +apr_status_t md_reg_update(md_reg_t *reg, apr_pool_t *p, + const char *name, const md_t *md, int fields) +{ + return md_util_pool_vdo(p_md_update, reg, p, name, md, fields, NULL); +} + +/**************************************************************************************************/ +/* certificate related */ + +static int ok_or_noent(apr_status_t rv) +{ + return (APR_SUCCESS == rv || APR_ENOENT == rv); +} + +static apr_status_t creds_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_reg_t *reg = baton; + md_pkey_t *privkey; + apr_array_header_t *pubcert; + md_creds_t *creds, **pcreds; + const md_t *md; + md_cert_state_t cert_state; + md_store_group_t group; + apr_status_t rv; + + pcreds = va_arg(ap, md_creds_t **); + group = (md_store_group_t)va_arg(ap, int); + md = va_arg(ap, const md_t *); + + if (ok_or_noent(rv = md_pkey_load(reg->store, group, md->name, &privkey, p)) + && ok_or_noent(rv = md_pubcert_load(reg->store, group, md->name, &pubcert, p))) { + rv = APR_SUCCESS; + + creds = apr_pcalloc(p, sizeof(*creds)); + creds->privkey = privkey; + if (pubcert && pubcert->nelts > 0) { + creds->pubcert = pubcert; + creds->cert = APR_ARRAY_IDX(pubcert, 0, md_cert_t *); + } + if (creds->cert) { + switch ((cert_state = md_cert_state_get(creds->cert))) { + case MD_CERT_VALID: + creds->expired = 0; + break; + case MD_CERT_EXPIRED: + creds->expired = 1; + break; + default: + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, ptemp, + "md %s has unexpected cert state: %d", md->name, cert_state); + rv = APR_ENOTIMPL; + break; + } + } + } + *pcreds = (APR_SUCCESS == rv)? creds : NULL; + return rv; +} + +apr_status_t md_reg_creds_get(const md_creds_t **pcreds, md_reg_t *reg, + md_store_group_t group, const md_t *md, apr_pool_t *p) +{ + apr_status_t rv = APR_SUCCESS; + md_creds_t *creds; + + rv = md_util_pool_vdo(creds_load, reg, p, &creds, group, md, NULL); + *pcreds = (APR_SUCCESS == rv)? creds : NULL; + return rv; +} + +/**************************************************************************************************/ +/* synching */ + +typedef struct { + apr_pool_t *p; + apr_array_header_t *store_mds; +} sync_ctx; + +static int do_add_md(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp) +{ + sync_ctx *ctx = baton; + + (void)store; + (void)ptemp; + APR_ARRAY_PUSH(ctx->store_mds, const md_t*) = md_clone(ctx->p, md); + return 1; +} + +static apr_status_t read_store_mds(md_reg_t *reg, sync_ctx *ctx) +{ + int rv; + + apr_array_clear(ctx->store_mds); + rv = md_store_md_iter(do_add_md, ctx, reg->store, ctx->p, MD_SG_DOMAINS, "*"); + if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + } + return rv; +} + +apr_status_t md_reg_set_props(md_reg_t *reg, apr_pool_t *p, int can_http, int can_https) +{ + if (reg->can_http != can_http || reg->can_https != can_https) { + md_json_t *json; + + reg->can_http = can_http; + reg->can_https = can_https; + + json = md_json_create(p); + md_json_setb(can_http, json, MD_KEY_PROTO, MD_KEY_HTTP, NULL); + md_json_setb(can_https, json, MD_KEY_PROTO, MD_KEY_HTTPS, NULL); + + return md_store_save(reg->store, p, MD_SG_NONE, NULL, MD_FN_HTTPD_JSON, MD_SV_JSON, json, 0); + } + return APR_SUCCESS; +} + +/** + * Procedure: + * 1. Collect all defined "managed domains" (MD). It does not matter where a MD is defined. + * All MDs need to be unique and have no overlaps in their domain names. + * Fail the config otherwise. Also, if a vhost matches an MD, it + * needs to *only* have ServerAliases from that MD. There can be no more than one + * matching MD for a vhost. But an MD can apply to several vhosts. + * 2. Synchronize with the persistent store. Iterate over all configured MDs and + * a. create them in the store if they do not already exist, neither under the + * name or with a common domain. + * b. compare domain lists from store and config, if + * - store has dns name in other MD than from config, remove dns name from store def, + * issue WARNING. + * - store misses dns name from config, add dns name and update store + * c. compare MD acme url/protocol, update if changed + */ +apr_status_t md_reg_sync(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, + apr_array_header_t *master_mds) +{ + sync_ctx ctx; + apr_status_t rv; + + ctx.p = ptemp; + ctx.store_mds = apr_array_make(ptemp,100, sizeof(md_t *)); + rv = read_store_mds(reg, &ctx); + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "sync: found %d mds in store", ctx.store_mds->nelts); + if (APR_SUCCESS == rv) { + int i, fields; + md_t *md, *config_md, *smd, *omd; + const char *common; + + for (i = 0; i < master_mds->nelts; ++i) { + md = APR_ARRAY_IDX(master_mds, i, md_t *); + + /* find the store md that is closest match for the configured md */ + smd = md_find_closest_match(ctx.store_mds, md); + if (smd) { + fields = 0; + + /* Once stored, we keep the name */ + if (strcmp(md->name, smd->name)) { + md->name = apr_pstrdup(p, smd->name); + } + + /* Make the stored domain list *exactly* the same, even if + * someone only changed upper/lowercase, we'd like to persist that. */ + if (!md_equal_domains(md, smd, 1)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "%s: domains changed", smd->name); + smd->domains = md_array_str_clone(ptemp, md->domains); + fields |= MD_UPD_DOMAINS; + } + + /* Look for other store mds which have domains now being part of smd */ + while (APR_SUCCESS == rv && (omd = md_get_by_dns_overlap(ctx.store_mds, md))) { + /* find the name now duplicate */ + common = md_common_name(md, omd); + assert(common); + + /* Is this md still configured or has it been abandoned in the config? */ + config_md = md_get_by_name(master_mds, omd->name); + if (config_md && md_contains(config_md, common, 0)) { + /* domain used in two configured mds, not allowed */ + rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, + "domain %s used in md %s and %s", + common, md->name, omd->name); + } + else { + /* remove it from the other md and update store, or, if it + * is now empty, move it into the archive */ + omd->domains = md_array_str_remove(ptemp, omd->domains, common, 0); + if (apr_is_empty_array(omd->domains)) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, + "All domains of the MD %s have moved elsewhere, " + " moving it to the archive. ", omd->name); + md_reg_remove(reg, ptemp, omd->name, 1); /* best effort */ + } + else { + rv = md_reg_update(reg, ptemp, omd->name, omd, MD_UPD_DOMAINS); + } + } + } + + if (MD_SVAL_UPDATE(md, smd, ca_url)) { + smd->ca_url = md->ca_url; + fields |= MD_UPD_CA_URL; + } + if (MD_SVAL_UPDATE(md, smd, ca_proto)) { + smd->ca_proto = md->ca_proto; + fields |= MD_UPD_CA_PROTO; + } + if (MD_SVAL_UPDATE(md, smd, ca_agreement)) { + smd->ca_agreement = md->ca_agreement; + fields |= MD_UPD_AGREEMENT; + } + if (MD_VAL_UPDATE(md, smd, transitive)) { + smd->transitive = md->transitive; + fields |= MD_UPD_TRANSITIVE; + } + if (MD_VAL_UPDATE(md, smd, drive_mode)) { + smd->drive_mode = md->drive_mode; + fields |= MD_UPD_DRIVE_MODE; + } + if (!apr_is_empty_array(md->contacts) + && !md_array_str_eq(md->contacts, smd->contacts, 0)) { + smd->contacts = md->contacts; + fields |= MD_UPD_CONTACTS; + } + if (MD_VAL_UPDATE(md, smd, renew_window) + || MD_VAL_UPDATE(md, smd, renew_norm)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "%s: update renew norm=%ld, window=%ld", + smd->name, (long)md->renew_norm, (long)md->renew_window); + smd->renew_norm = md->renew_norm; + smd->renew_window = md->renew_window; + fields |= MD_UPD_RENEW_WINDOW; + } + if (md->ca_challenges) { + md->ca_challenges = md_array_str_compact(p, md->ca_challenges, 0); + if (!smd->ca_challenges + || !md_array_str_eq(md->ca_challenges, smd->ca_challenges, 0)) { + smd->ca_challenges = apr_array_copy(ptemp, md->ca_challenges); + fields |= MD_UPD_CA_CHALLENGES; + } + } + else if (smd->ca_challenges) { + smd->ca_challenges = NULL; + fields |= MD_UPD_CA_CHALLENGES; + } + if (!md_pkey_spec_eq(md->pkey_spec, smd->pkey_spec)) { + fields |= MD_UPD_PKEY_SPEC; + smd->pkey_spec = NULL; + if (md->pkey_spec) { + smd->pkey_spec = apr_pmemdup(p, md->pkey_spec, sizeof(md_pkey_spec_t)); + } + } + if (MD_VAL_UPDATE(md, smd, require_https)) { + smd->require_https = md->require_https; + fields |= MD_UPD_REQUIRE_HTTPS; + } + if (MD_VAL_UPDATE(md, smd, must_staple)) { + smd->must_staple = md->must_staple; + fields |= MD_UPD_MUST_STAPLE; + } + + if (fields) { + rv = md_reg_update(reg, ptemp, smd->name, smd, fields); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "md %s updated", smd->name); + } + } + else { + /* new managed domain */ + rv = md_reg_add(reg, md, ptemp); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "new md %s added", md->name); + } + } + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "loading mds"); + } + + return rv; +} + +apr_status_t md_reg_remove(md_reg_t *reg, apr_pool_t *p, const char *name, int archive) +{ + return md_store_move(reg->store, p, MD_SG_DOMAINS, MD_SG_ARCHIVE, name, archive); +} + + +/**************************************************************************************************/ +/* driving */ + +static apr_status_t init_proto_driver(md_proto_driver_t *driver, const md_proto_t *proto, + md_reg_t *reg, const md_t *md, + const char *challenge, int reset, apr_pool_t *p) +{ + apr_status_t rv = APR_SUCCESS; + + /* If this registry instance was not synched before (and obtained server + * properties that way), read them from the store. + */ + driver->proto = proto; + driver->p = p; + driver->challenge = challenge; + driver->can_http = reg->can_http; + driver->can_https = reg->can_https; + driver->reg = reg; + driver->store = md_reg_store_get(reg); + driver->proxy_url = reg->proxy_url; + driver->md = md; + driver->reset = reset; + + return rv; +} + +static apr_status_t run_stage(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_reg_t *reg = baton; + const md_proto_t *proto; + const md_t *md; + int reset; + md_proto_driver_t *driver; + const char *challenge; + apr_time_t *pvalid_from; + apr_status_t rv; + + (void)p; + proto = va_arg(ap, const md_proto_t *); + md = va_arg(ap, const md_t *); + challenge = va_arg(ap, const char *); + reset = va_arg(ap, int); + pvalid_from = va_arg(ap, apr_time_t*); + + driver = apr_pcalloc(ptemp, sizeof(*driver)); + rv = init_proto_driver(driver, proto, reg, md, challenge, reset, ptemp); + if (APR_SUCCESS == rv && + APR_SUCCESS == (rv = proto->init(driver))) { + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "%s: run staging", md->name); + rv = proto->stage(driver); + + if (APR_SUCCESS == rv && pvalid_from) { + *pvalid_from = driver->stage_valid_from; + } + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "%s: staging done", md->name); + return rv; +} + +apr_status_t md_reg_stage(md_reg_t *reg, const md_t *md, const char *challenge, + int reset, apr_time_t *pvalid_from, apr_pool_t *p) +{ + const md_proto_t *proto; + + if (!md->ca_proto) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "md %s has no CA protocol", md->name); + ((md_t *)md)->state = MD_S_ERROR; + return APR_SUCCESS; + } + + proto = apr_hash_get(reg->protos, md->ca_proto, (apr_ssize_t)strlen(md->ca_proto)); + if (!proto) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, + "md %s has unknown CA protocol: %s", md->name, md->ca_proto); + ((md_t *)md)->state = MD_S_ERROR; + return APR_EINVAL; + } + + return md_util_pool_vdo(run_stage, reg, p, proto, md, challenge, reset, pvalid_from, NULL); +} + +static apr_status_t run_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_reg_t *reg = baton; + const char *name; + const md_proto_t *proto; + const md_t *md, *nmd; + md_proto_driver_t *driver; + apr_status_t rv; + + name = va_arg(ap, const char *); + + if (APR_STATUS_IS_ENOENT(rv = md_load(reg->store, MD_SG_STAGING, name, NULL, ptemp))) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, ptemp, "%s: nothing staged", name); + return APR_ENOENT; + } + + md = md_reg_get(reg, name, p); + if (!md) { + return APR_ENOENT; + } + + if (!md->ca_proto) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "md %s has no CA protocol", name); + ((md_t *)md)->state = MD_S_ERROR; + return APR_EINVAL; + } + + proto = apr_hash_get(reg->protos, md->ca_proto, (apr_ssize_t)strlen(md->ca_proto)); + if (!proto) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, + "md %s has unknown CA protocol: %s", md->name, md->ca_proto); + ((md_t *)md)->state = MD_S_ERROR; + return APR_EINVAL; + } + + driver = apr_pcalloc(ptemp, sizeof(*driver)); + init_proto_driver(driver, proto, reg, md, NULL, 0, ptemp); + + if (APR_SUCCESS == (rv = proto->init(driver))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "%s: run load", md->name); + + if (APR_SUCCESS == (rv = proto->preload(driver, MD_SG_TMP))) { + /* swap */ + rv = md_store_move(reg->store, p, MD_SG_TMP, MD_SG_DOMAINS, md->name, 1); + if (APR_SUCCESS == rv) { + /* load again */ + nmd = md_reg_get(reg, md->name, p); + if (!nmd) { + rv = APR_ENOENT; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "loading md after staging"); + } + else if (nmd->state != MD_S_COMPLETE) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, + "md has state %d after load", nmd->state); + } + + md_store_purge(reg->store, p, MD_SG_STAGING, md->name); + md_store_purge(reg->store, p, MD_SG_CHALLENGES, md->name); + } + } + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "%s: load done", md->name); + return rv; +} + +apr_status_t md_reg_load(md_reg_t *reg, const char *name, apr_pool_t *p) +{ + return md_util_pool_vdo(run_load, reg, p, name, NULL); +} + diff --git a/modules/md/md_reg.h b/modules/md/md_reg.h new file mode 100644 index 0000000..d976b7f --- /dev/null +++ b/modules/md/md_reg.h @@ -0,0 +1,179 @@ +/* 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. + */ + +#ifndef mod_md_md_reg_h +#define mod_md_md_reg_h + +struct apr_hash_t; +struct apr_array_header_t; +struct md_store_t; +struct md_pkey_t; +struct md_cert_t; + +/** + * A registry for managed domains with a md_store_t as persistence. + * + */ +typedef struct md_reg_t md_reg_t; + +/** + * Initialize the registry, using the pool and loading any existing information + * from the store. + */ +apr_status_t md_reg_init(md_reg_t **preg, apr_pool_t *pm, struct md_store_t *store, + const char *proxy_url); + +struct md_store_t *md_reg_store_get(md_reg_t *reg); + +apr_status_t md_reg_set_props(md_reg_t *reg, apr_pool_t *p, int can_http, int can_https); + +/** + * Add a new md to the registry. This will check the name for uniqueness and + * that domain names do not overlap with already existing mds. + */ +apr_status_t md_reg_add(md_reg_t *reg, md_t *md, apr_pool_t *p); + +/** + * Find the md, if any, that contains the given domain name. + * NULL if none found. + */ +md_t *md_reg_find(md_reg_t *reg, const char *domain, apr_pool_t *p); + +/** + * Find one md, which domain names overlap with the given md and that has a different + * name. There may be more than one existing md that overlaps. It is not defined + * which one will be returned. + */ +md_t *md_reg_find_overlap(md_reg_t *reg, const md_t *md, const char **pdomain, apr_pool_t *p); + +/** + * Get the md with the given unique name. NULL if it does not exist. + * Will update the md->state. + */ +md_t *md_reg_get(md_reg_t *reg, const char *name, apr_pool_t *p); + +/** + * Assess the capability and need to driving this managed domain. + */ +apr_status_t md_reg_assess(md_reg_t *reg, md_t *md, int *perrored, int *prenew, apr_pool_t *p); + +/** + * Callback invoked for every md in the registry. If 0 is returned, iteration stops. + */ +typedef int md_reg_do_cb(void *baton, md_reg_t *reg, md_t *md); + +/** + * Invoke callback for all mds in this registry. Order is not guaranteed. + * If the callback returns 0, iteration stops. Returns 0 if iteration was + * aborted. + */ +int md_reg_do(md_reg_do_cb *cb, void *baton, md_reg_t *reg, apr_pool_t *p); + +/** + * Bitmask for fields that are updated. + */ +#define MD_UPD_DOMAINS 0x0001 +#define MD_UPD_CA_URL 0x0002 +#define MD_UPD_CA_PROTO 0x0004 +#define MD_UPD_CA_ACCOUNT 0x0008 +#define MD_UPD_CONTACTS 0x0010 +#define MD_UPD_AGREEMENT 0x0020 +#define MD_UPD_CERT_URL 0x0040 +#define MD_UPD_DRIVE_MODE 0x0080 +#define MD_UPD_RENEW_WINDOW 0x0100 +#define MD_UPD_CA_CHALLENGES 0x0200 +#define MD_UPD_PKEY_SPEC 0x0400 +#define MD_UPD_REQUIRE_HTTPS 0x0800 +#define MD_UPD_TRANSITIVE 0x1000 +#define MD_UPD_MUST_STAPLE 0x2000 +#define MD_UPD_ALL 0x7FFFFFFF + +/** + * Update the given fields for the managed domain. Take the new + * values from the given md, all other values remain unchanged. + */ +apr_status_t md_reg_update(md_reg_t *reg, apr_pool_t *p, + const char *name, const md_t *md, int fields); + +/** + * Get the credentials available for the managed domain md. Returns APR_ENOENT + * when none is available. The returned values are immutable. + */ +apr_status_t md_reg_creds_get(const md_creds_t **pcreds, md_reg_t *reg, + md_store_group_t group, const md_t *md, apr_pool_t *p); + +apr_status_t md_reg_get_cred_files(md_reg_t *reg, const md_t *md, apr_pool_t *p, + const char **pkeyfile, const char **pcertfile); + +/** + * Synchronise the give master mds with the store. + */ +apr_status_t md_reg_sync(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, + apr_array_header_t *master_mds); + +apr_status_t md_reg_remove(md_reg_t *reg, apr_pool_t *p, const char *name, int archive); + +/**************************************************************************************************/ +/* protocol drivers */ + +typedef struct md_proto_t md_proto_t; + +typedef struct md_proto_driver_t md_proto_driver_t; + +struct md_proto_driver_t { + const md_proto_t *proto; + apr_pool_t *p; + const char *challenge; + int can_http; + int can_https; + struct md_store_t *store; + md_reg_t *reg; + const md_t *md; + void *baton; + int reset; + apr_time_t stage_valid_from; + const char *proxy_url; +}; + +typedef apr_status_t md_proto_init_cb(md_proto_driver_t *driver); +typedef apr_status_t md_proto_stage_cb(md_proto_driver_t *driver); +typedef apr_status_t md_proto_preload_cb(md_proto_driver_t *driver, md_store_group_t group); + +struct md_proto_t { + const char *protocol; + md_proto_init_cb *init; + md_proto_stage_cb *stage; + md_proto_preload_cb *preload; +}; + + +/** + * Stage a new credentials set for the given managed domain in a separate location + * without interfering with any existing credentials. + */ +apr_status_t md_reg_stage(md_reg_t *reg, const md_t *md, + const char *challenge, int reset, + apr_time_t *pvalid_from, apr_pool_t *p); + +/** + * Load a staged set of new credentials for the managed domain. This will archive + * any existing credential data and make the staged set the new live one. + * If staging is incomplete or missing, the load will fail and all credentials remain + * as they are. + */ +apr_status_t md_reg_load(md_reg_t *reg, const char *name, apr_pool_t *p); + +#endif /* mod_md_md_reg_h */ diff --git a/modules/md/md_store.c b/modules/md/md_store.c new file mode 100644 index 0000000..a047ff3 --- /dev/null +++ b/modules/md/md_store.c @@ -0,0 +1,319 @@ +/* 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. + */ + +#include <assert.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> + +#include <apr_lib.h> +#include <apr_file_info.h> +#include <apr_file_io.h> +#include <apr_fnmatch.h> +#include <apr_hash.h> +#include <apr_strings.h> + +#include "md.h" +#include "md_crypt.h" +#include "md_log.h" +#include "md_json.h" +#include "md_store.h" +#include "md_util.h" + +/**************************************************************************************************/ +/* generic callback handling */ + +#define ASPECT_MD "md.json" +#define ASPECT_CERT "cert.pem" +#define ASPECT_PKEY "key.pem" +#define ASPECT_CHAIN "chain.pem" + +#define GNAME_ACCOUNTS +#define GNAME_CHALLENGES +#define GNAME_DOMAINS +#define GNAME_STAGING +#define GNAME_ARCHIVE + +static const char *GROUP_NAME[] = { + "none", + "accounts", + "challenges", + "domains", + "staging", + "archive", + "tmp", + NULL +}; + +const char *md_store_group_name(int group) +{ + if ((size_t)group < sizeof(GROUP_NAME)/sizeof(GROUP_NAME[0])) { + return GROUP_NAME[group]; + } + return "UNKNOWN"; +} + +void md_store_destroy(md_store_t *store) +{ + if (store->destroy) store->destroy(store); +} + +apr_status_t md_store_load(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void **pdata, + apr_pool_t *p) +{ + return store->load(store, group, name, aspect, vtype, pdata, p); +} + +apr_status_t md_store_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void *data, + int create) +{ + return store->save(store, p, group, name, aspect, vtype, data, create); +} + +apr_status_t md_store_remove(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p, int force) +{ + return store->remove(store, group, name, aspect, p, force); +} + +apr_status_t md_store_purge(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name) +{ + return store->purge(store, p, group, name); +} + +apr_status_t md_store_iter(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern, + const char *aspect, md_store_vtype_t vtype) +{ + return store->iterate(inspect, baton, store, p, group, pattern, aspect, vtype); +} + +apr_status_t md_store_load_json(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + struct md_json_t **pdata, apr_pool_t *p) +{ + return md_store_load(store, group, name, aspect, MD_SV_JSON, (void**)pdata, p); +} + +apr_status_t md_store_save_json(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, const char *aspect, + struct md_json_t *data, int create) +{ + return md_store_save(store, p, group, name, aspect, MD_SV_JSON, (void*)data, create); +} + +apr_status_t md_store_move(md_store_t *store, apr_pool_t *p, + md_store_group_t from, md_store_group_t to, + const char *name, int archive) +{ + return store->move(store, p, from, to, name, archive); +} + +apr_status_t md_store_get_fname(const char **pfname, + md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p) +{ + if (store->get_fname) { + return store->get_fname(pfname, store, group, name, aspect, p); + } + return APR_ENOTIMPL; +} + +int md_store_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2, + const char *name, const char *aspect, apr_pool_t *p) +{ + return store->is_newer(store, group1, group2, name, aspect, p); +} + +/**************************************************************************************************/ +/* convenience */ + +typedef struct { + md_store_t *store; + md_store_group_t group; +} md_group_ctx; + +apr_status_t md_load(md_store_t *store, md_store_group_t group, + const char *name, md_t **pmd, apr_pool_t *p) +{ + md_json_t *json; + apr_status_t rv; + + rv = md_store_load_json(store, group, name, MD_FN_MD, pmd? &json : NULL, p); + if (APR_SUCCESS == rv) { + if (pmd) { + *pmd = md_from_json(json, p); + } + return APR_SUCCESS; + } + return rv; +} + +static apr_status_t p_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_group_ctx *ctx = baton; + md_json_t *json; + md_t *md; + int create; + + md = va_arg(ap, md_t *); + create = va_arg(ap, int); + + json = md_to_json(md, ptemp); + assert(json); + assert(md->name); + return md_store_save_json(ctx->store, p, ctx->group, md->name, MD_FN_MD, json, create); +} + +apr_status_t md_save(md_store_t *store, apr_pool_t *p, + md_store_group_t group, md_t *md, int create) +{ + md_group_ctx ctx; + + ctx.store = store; + ctx.group = group; + return md_util_pool_vdo(p_save, &ctx, p, md, create, NULL); +} + +static apr_status_t p_remove(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_group_ctx *ctx = baton; + const char *name; + int force; + + (void)p; + name = va_arg(ap, const char *); + force = va_arg(ap, int); + + assert(name); + return md_store_remove(ctx->store, ctx->group, name, MD_FN_MD, ptemp, force); +} + +apr_status_t md_remove(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name, int force) +{ + md_group_ctx ctx; + + ctx.store = store; + ctx.group = group; + return md_util_pool_vdo(p_remove, &ctx, p, name, force, NULL); +} + +int md_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2, + const char *name, apr_pool_t *p) +{ + return md_store_is_newer(store, group1, group2, name, MD_FN_MD, p); +} + + +typedef struct { + apr_pool_t *p; + apr_array_header_t *mds; +} md_load_ctx; + +apr_status_t md_pkey_load(md_store_t *store, md_store_group_t group, const char *name, + md_pkey_t **ppkey, apr_pool_t *p) +{ + return md_store_load(store, group, name, MD_FN_PRIVKEY, MD_SV_PKEY, (void**)ppkey, p); +} + +apr_status_t md_pkey_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *name, + struct md_pkey_t *pkey, int create) +{ + return md_store_save(store, p, group, name, MD_FN_PRIVKEY, MD_SV_PKEY, pkey, create); +} + +apr_status_t md_cert_load(md_store_t *store, md_store_group_t group, const char *name, + struct md_cert_t **pcert, apr_pool_t *p) +{ + return md_store_load(store, group, name, MD_FN_CERT, MD_SV_CERT, (void**)pcert, p); +} + +apr_status_t md_cert_save(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name, + struct md_cert_t *cert, int create) +{ + return md_store_save(store, p, group, name, MD_FN_CERT, MD_SV_CERT, cert, create); +} + +apr_status_t md_chain_load(md_store_t *store, md_store_group_t group, const char *name, + struct apr_array_header_t **pchain, apr_pool_t *p) +{ + return md_store_load(store, group, name, MD_FN_CHAIN, MD_SV_CHAIN, (void**)pchain, p); +} + +apr_status_t md_chain_save(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name, + struct apr_array_header_t *chain, int create) +{ + return md_store_save(store, p, group, name, MD_FN_CHAIN, MD_SV_CHAIN, chain, create); +} + +apr_status_t md_pubcert_load(md_store_t *store, md_store_group_t group, const char *name, + struct apr_array_header_t **ppubcert, apr_pool_t *p) +{ + return md_store_load(store, group, name, MD_FN_PUBCERT, MD_SV_CHAIN, (void**)ppubcert, p); +} + +apr_status_t md_pubcert_save(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name, + struct apr_array_header_t *pubcert, int create) +{ + return md_store_save(store, p, group, name, MD_FN_PUBCERT, MD_SV_CHAIN, pubcert, create); +} + +typedef struct { + md_store_t *store; + md_store_group_t group; + const char *pattern; + const char *aspect; + md_store_md_inspect *inspect; + void *baton; +} inspect_md_ctx; + +static int insp_md(void *baton, const char *name, const char *aspect, + md_store_vtype_t vtype, void *value, apr_pool_t *ptemp) +{ + inspect_md_ctx *ctx = baton; + + if (!strcmp(MD_FN_MD, aspect) && vtype == MD_SV_JSON) { + md_t *md = md_from_json(value, ptemp); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "inspecting md at: %s", name); + return ctx->inspect(ctx->baton, ctx->store, md, ptemp); + } + return 1; +} + +apr_status_t md_store_md_iter(md_store_md_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern) +{ + inspect_md_ctx ctx; + + ctx.store = store; + ctx.group = group; + ctx.inspect = inspect; + ctx.baton = baton; + + return md_store_iter(insp_md, &ctx, store, p, group, pattern, MD_FN_MD, MD_SV_JSON); +} + diff --git a/modules/md/md_store.h b/modules/md/md_store.h new file mode 100644 index 0000000..5825189 --- /dev/null +++ b/modules/md/md_store.h @@ -0,0 +1,157 @@ +/* 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. + */ + +#ifndef mod_md_md_store_h +#define mod_md_md_store_h + +struct apr_array_header_t; +struct md_cert_t; +struct md_pkey_t; + +typedef struct md_store_t md_store_t; + +typedef void md_store_destroy_cb(md_store_t *store); + +const char *md_store_group_name(int group); + + +typedef apr_status_t md_store_load_cb(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void **pvalue, + apr_pool_t *p); +typedef apr_status_t md_store_save_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void *value, + int create); +typedef apr_status_t md_store_remove_cb(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p, int force); +typedef apr_status_t md_store_purge_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name); + +typedef int md_store_inspect(void *baton, const char *name, const char *aspect, + md_store_vtype_t vtype, void *value, apr_pool_t *ptemp); + +typedef apr_status_t md_store_iter_cb(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern, + const char *aspect, md_store_vtype_t vtype); + +typedef apr_status_t md_store_move_cb(md_store_t *store, apr_pool_t *p, md_store_group_t from, + md_store_group_t to, const char *name, int archive); + +typedef apr_status_t md_store_get_fname_cb(const char **pfname, + md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p); + +typedef int md_store_is_newer_cb(md_store_t *store, + md_store_group_t group1, md_store_group_t group2, + const char *name, const char *aspect, apr_pool_t *p); + +struct md_store_t { + md_store_destroy_cb *destroy; + + md_store_save_cb *save; + md_store_load_cb *load; + md_store_remove_cb *remove; + md_store_move_cb *move; + md_store_iter_cb *iterate; + md_store_purge_cb *purge; + md_store_get_fname_cb *get_fname; + md_store_is_newer_cb *is_newer; +}; + +void md_store_destroy(md_store_t *store); + +apr_status_t md_store_load_json(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + struct md_json_t **pdata, apr_pool_t *p); +apr_status_t md_store_save_json(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, const char *aspect, + struct md_json_t *data, int create); + + +apr_status_t md_store_load(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void **pdata, + apr_pool_t *p); +apr_status_t md_store_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void *data, + int create); +apr_status_t md_store_remove(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p, int force); +apr_status_t md_store_purge(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name); + + +apr_status_t md_store_iter(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern, + const char *aspect, md_store_vtype_t vtype); + +apr_status_t md_store_move(md_store_t *store, apr_pool_t *p, + md_store_group_t from, md_store_group_t to, + const char *name, int archive); + +apr_status_t md_store_get_fname(const char **pfname, + md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p); + +int md_store_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2, + const char *name, const char *aspect, apr_pool_t *p); + +/**************************************************************************************************/ +/* Storage handling utils */ + +apr_status_t md_load(md_store_t *store, md_store_group_t group, + const char *name, md_t **pmd, apr_pool_t *p); +apr_status_t md_save(struct md_store_t *store, apr_pool_t *p, md_store_group_t group, + md_t *md, int create); +apr_status_t md_remove(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, int force); + +int md_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2, + const char *name, apr_pool_t *p); + +typedef int md_store_md_inspect(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp); + +apr_status_t md_store_md_iter(md_store_md_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern); + + +apr_status_t md_pkey_load(md_store_t *store, md_store_group_t group, + const char *name, struct md_pkey_t **ppkey, apr_pool_t *p); +apr_status_t md_pkey_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, struct md_pkey_t *pkey, int create); +apr_status_t md_cert_load(md_store_t *store, md_store_group_t group, + const char *name, struct md_cert_t **pcert, apr_pool_t *p); +apr_status_t md_cert_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, struct md_cert_t *cert, int create); +apr_status_t md_chain_load(md_store_t *store, md_store_group_t group, + const char *name, struct apr_array_header_t **pchain, apr_pool_t *p); +apr_status_t md_chain_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, struct apr_array_header_t *chain, int create); + +apr_status_t md_pubcert_load(md_store_t *store, md_store_group_t group, const char *name, + struct apr_array_header_t **ppubcert, apr_pool_t *p); +apr_status_t md_pubcert_save(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name, + struct apr_array_header_t *pubcert, int create); + + +#endif /* mod_md_md_store_h */ diff --git a/modules/md/md_store_fs.c b/modules/md/md_store_fs.c new file mode 100644 index 0000000..f399cea --- /dev/null +++ b/modules/md/md_store_fs.c @@ -0,0 +1,883 @@ +/* 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. + */ + +#include <assert.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> + +#include <apr_lib.h> +#include <apr_file_info.h> +#include <apr_file_io.h> +#include <apr_fnmatch.h> +#include <apr_hash.h> +#include <apr_strings.h> + +#include "md.h" +#include "md_crypt.h" +#include "md_json.h" +#include "md_log.h" +#include "md_store.h" +#include "md_store_fs.h" +#include "md_util.h" +#include "md_version.h" + +/**************************************************************************************************/ +/* file system based implementation of md_store_t */ + +#define MD_STORE_VERSION 3 + +typedef struct { + apr_fileperms_t dir; + apr_fileperms_t file; +} perms_t; + +typedef struct md_store_fs_t md_store_fs_t; +struct md_store_fs_t { + md_store_t s; + + const char *base; /* base directory of store */ + perms_t def_perms; + perms_t group_perms[MD_SG_COUNT]; + md_store_fs_cb *event_cb; + void *event_baton; + + const unsigned char *key; + apr_size_t key_len; + int plain_pkey[MD_SG_COUNT]; + + int port_80; + int port_443; +}; + +#define FS_STORE(store) (md_store_fs_t*)(((char*)store)-offsetof(md_store_fs_t, s)) +#define FS_STORE_JSON "md_store.json" +#define FS_STORE_KLEN 48 + +static apr_status_t fs_load(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void **pvalue, apr_pool_t *p); +static apr_status_t fs_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void *value, int create); +static apr_status_t fs_remove(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p, int force); +static apr_status_t fs_purge(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name); +static apr_status_t fs_move(md_store_t *store, apr_pool_t *p, + md_store_group_t from, md_store_group_t to, + const char *name, int archive); +static apr_status_t fs_iterate(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern, + const char *aspect, md_store_vtype_t vtype); + +static apr_status_t fs_get_fname(const char **pfname, + md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p); +static int fs_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2, + const char *name, const char *aspect, apr_pool_t *p); + +static apr_status_t init_store_file(md_store_fs_t *s_fs, const char *fname, + apr_pool_t *p, apr_pool_t *ptemp) +{ + md_json_t *json = md_json_create(p); + const char *key64; + unsigned char *key; + apr_status_t rv; + + md_json_setn(MD_STORE_VERSION, json, MD_KEY_STORE, MD_KEY_VERSION, NULL); + + s_fs->key_len = FS_STORE_KLEN; + s_fs->key = key = apr_pcalloc(p, FS_STORE_KLEN); + if (APR_SUCCESS != (rv = md_rand_bytes(key, s_fs->key_len, p))) { + return rv; + } + + key64 = md_util_base64url_encode((char *)key, s_fs->key_len, ptemp); + md_json_sets(key64, json, MD_KEY_KEY, NULL); + rv = md_json_fcreatex(json, ptemp, MD_JSON_FMT_INDENT, fname, MD_FPROT_F_UONLY); + memset((char*)key64, 0, strlen(key64)); + + return rv; +} + +static apr_status_t rename_pkey(void *baton, apr_pool_t *p, apr_pool_t *ptemp, + const char *dir, const char *name, + apr_filetype_e ftype) +{ + const char *from, *to; + apr_status_t rv = APR_SUCCESS; + MD_CHK_VARS; + + (void)baton; + (void)ftype; + if ( MD_OK(md_util_path_merge(&from, ptemp, dir, name, NULL)) + && MD_OK(md_util_path_merge(&to, ptemp, dir, MD_FN_PRIVKEY, NULL))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "renaming %s/%s to %s", + dir, name, MD_FN_PRIVKEY); + return apr_file_rename(from, to, ptemp); + } + return rv; +} + +static apr_status_t mk_pubcert(void *baton, apr_pool_t *p, apr_pool_t *ptemp, + const char *dir, const char *name, + apr_filetype_e ftype) +{ + md_cert_t *cert; + apr_array_header_t *chain, *pubcert; + const char *fname, *fpubcert; + apr_status_t rv = APR_SUCCESS; + MD_CHK_VARS; + + (void)baton; + (void)ftype; + (void)p; + if ( MD_OK(md_util_path_merge(&fpubcert, ptemp, dir, MD_FN_PUBCERT, NULL)) + && MD_IS_ERR(md_chain_fload(&pubcert, ptemp, fpubcert), ENOENT) + && MD_OK(md_util_path_merge(&fname, ptemp, dir, name, NULL)) + && MD_OK(md_cert_fload(&cert, ptemp, fname)) + && MD_OK(md_util_path_merge(&fname, ptemp, dir, MD_FN_CHAIN, NULL))) { + + rv = md_chain_fload(&chain, ptemp, fname); + if (APR_STATUS_IS_ENOENT(rv)) { + chain = apr_array_make(ptemp, 1, sizeof(md_cert_t*)); + rv = APR_SUCCESS; + } + if (APR_SUCCESS == rv) { + pubcert = apr_array_make(ptemp, chain->nelts + 1, sizeof(md_cert_t*)); + APR_ARRAY_PUSH(pubcert, md_cert_t *) = cert; + apr_array_cat(pubcert, chain); + rv = md_chain_fsave(pubcert, ptemp, fpubcert, MD_FPROT_F_UONLY); + } + } + return rv; +} + +static apr_status_t upgrade_from_1_0(md_store_fs_t *s_fs, apr_pool_t *p, apr_pool_t *ptemp) +{ + md_store_group_t g; + apr_status_t rv = APR_SUCCESS; + + (void)ptemp; + /* Migrate pkey.pem -> privkey.pem */ + for (g = MD_SG_NONE; g < MD_SG_COUNT && APR_SUCCESS == rv; ++g) { + rv = md_util_files_do(rename_pkey, s_fs, p, s_fs->base, + md_store_group_name(g), "*", "pkey.pem", NULL); + } + /* Generate fullcert.pem from cert.pem and chain.pem where missing */ + rv = md_util_files_do(mk_pubcert, s_fs, p, s_fs->base, + md_store_group_name(MD_SG_DOMAINS), "*", MD_FN_CERT, NULL); + rv = md_util_files_do(mk_pubcert, s_fs, p, s_fs->base, + md_store_group_name(MD_SG_ARCHIVE), "*", MD_FN_CERT, NULL); + + return rv; +} + +static apr_status_t read_store_file(md_store_fs_t *s_fs, const char *fname, + apr_pool_t *p, apr_pool_t *ptemp) +{ + md_json_t *json; + const char *key64, *key; + apr_status_t rv; + double store_version; + MD_CHK_VARS; + + if (MD_OK(md_json_readf(&json, p, fname))) { + store_version = md_json_getn(json, MD_KEY_STORE, MD_KEY_VERSION, NULL); + if (store_version <= 0.0) { + /* ok, an old one, compatible to 1.0 */ + store_version = 1.0; + } + if (store_version > MD_STORE_VERSION) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "version too new: %f", store_version); + return APR_EINVAL; + } + + key64 = md_json_dups(p, json, MD_KEY_KEY, NULL); + if (!key64) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "missing key: %s", MD_KEY_KEY); + return APR_EINVAL; + } + + s_fs->key_len = md_util_base64url_decode(&key, key64, p); + s_fs->key = (const unsigned char*)key; + if (s_fs->key_len != FS_STORE_KLEN) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "key length unexpected: %" APR_SIZE_T_FMT, + s_fs->key_len); + return APR_EINVAL; + } + + /* Need to migrate format? */ + if (store_version < MD_STORE_VERSION) { + if (store_version <= 1.0) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "migrating store v1 -> v2"); + rv = upgrade_from_1_0(s_fs, p, ptemp); + } + if (store_version <= 2.0) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "migrating store v2 -> v3"); + md_json_del(json, MD_KEY_VERSION, NULL); + } + + if (APR_SUCCESS == rv) { + md_json_setn(MD_STORE_VERSION, json, MD_KEY_STORE, MD_KEY_VERSION, NULL); + rv = md_json_freplace(json, ptemp, MD_JSON_FMT_INDENT, fname, MD_FPROT_F_UONLY); + } + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, "migrated store"); + } + } + return rv; +} + +static apr_status_t setup_store_file(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_fs_t *s_fs = baton; + const char *fname; + apr_status_t rv; + MD_CHK_VARS; + + (void)ap; + s_fs->plain_pkey[MD_SG_DOMAINS] = 1; + s_fs->plain_pkey[MD_SG_TMP] = 1; + + if (!MD_OK(md_util_path_merge(&fname, ptemp, s_fs->base, FS_STORE_JSON, NULL))) { + return rv; + } + +read: + if (MD_OK(md_util_is_file(fname, ptemp))) { + rv = read_store_file(s_fs, fname, p, ptemp); + } + else if (APR_STATUS_IS_ENOENT(rv) + && MD_IS_ERR(init_store_file(s_fs, fname, p, ptemp), EEXIST)) { + goto read; + } + return rv; +} + +apr_status_t md_store_fs_init(md_store_t **pstore, apr_pool_t *p, const char *path) +{ + md_store_fs_t *s_fs; + apr_status_t rv = APR_SUCCESS; + MD_CHK_VARS; + + s_fs = apr_pcalloc(p, sizeof(*s_fs)); + + s_fs->s.load = fs_load; + s_fs->s.save = fs_save; + s_fs->s.remove = fs_remove; + s_fs->s.move = fs_move; + s_fs->s.purge = fs_purge; + s_fs->s.iterate = fs_iterate; + s_fs->s.get_fname = fs_get_fname; + s_fs->s.is_newer = fs_is_newer; + + /* by default, everything is only readable by the current user */ + s_fs->def_perms.dir = MD_FPROT_D_UONLY; + s_fs->def_perms.file = MD_FPROT_F_UONLY; + + /* Account information needs to be accessible to httpd child processes. + * private keys are, similar to staging, encrypted. */ + s_fs->group_perms[MD_SG_ACCOUNTS].dir = MD_FPROT_D_UALL_WREAD; + s_fs->group_perms[MD_SG_ACCOUNTS].file = MD_FPROT_F_UALL_WREAD; + s_fs->group_perms[MD_SG_STAGING].dir = MD_FPROT_D_UALL_WREAD; + s_fs->group_perms[MD_SG_STAGING].file = MD_FPROT_F_UALL_WREAD; + /* challenges dir and files are readable by all, no secrets involved */ + s_fs->group_perms[MD_SG_CHALLENGES].dir = MD_FPROT_D_UALL_WREAD; + s_fs->group_perms[MD_SG_CHALLENGES].file = MD_FPROT_F_UALL_WREAD; + + s_fs->base = apr_pstrdup(p, path); + + if (MD_IS_ERR(md_util_is_dir(s_fs->base, p), ENOENT) + && MD_OK(apr_dir_make_recursive(s_fs->base, s_fs->def_perms.dir, p))) { + rv = apr_file_perms_set(s_fs->base, MD_FPROT_D_UALL_WREAD); + if (APR_STATUS_IS_ENOTIMPL(rv)) { + rv = APR_SUCCESS; + } + } + + if ((APR_SUCCESS != rv) || !MD_OK(md_util_pool_vdo(setup_store_file, s_fs, p, NULL))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "init fs store at %s", path); + } + *pstore = (rv == APR_SUCCESS)? &(s_fs->s) : NULL; + return rv; +} + +apr_status_t md_store_fs_default_perms_set(md_store_t *store, + apr_fileperms_t file_perms, + apr_fileperms_t dir_perms) +{ + md_store_fs_t *s_fs = FS_STORE(store); + + s_fs->def_perms.file = file_perms; + s_fs->def_perms.dir = dir_perms; + return APR_SUCCESS; +} + +apr_status_t md_store_fs_group_perms_set(md_store_t *store, md_store_group_t group, + apr_fileperms_t file_perms, + apr_fileperms_t dir_perms) +{ + md_store_fs_t *s_fs = FS_STORE(store); + + if (group >= (sizeof(s_fs->group_perms)/sizeof(s_fs->group_perms[0]))) { + return APR_ENOTIMPL; + } + s_fs->group_perms[group].file = file_perms; + s_fs->group_perms[group].dir = dir_perms; + return APR_SUCCESS; +} + +apr_status_t md_store_fs_set_event_cb(struct md_store_t *store, md_store_fs_cb *cb, void *baton) +{ + md_store_fs_t *s_fs = FS_STORE(store); + + s_fs->event_cb = cb; + s_fs->event_baton = baton; + return APR_SUCCESS; +} + +static const perms_t *gperms(md_store_fs_t *s_fs, md_store_group_t group) +{ + if (group >= (sizeof(s_fs->group_perms)/sizeof(s_fs->group_perms[0])) + || !s_fs->group_perms[group].dir) { + return &s_fs->def_perms; + } + return &s_fs->group_perms[group]; +} + +static apr_status_t fs_get_fname(const char **pfname, + md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p) +{ + md_store_fs_t *s_fs = FS_STORE(store); + if (group == MD_SG_NONE) { + return md_util_path_merge(pfname, p, s_fs->base, aspect, NULL); + } + return md_util_path_merge(pfname, p, + s_fs->base, md_store_group_name(group), name, aspect, NULL); +} + +static apr_status_t fs_get_dname(const char **pdname, + md_store_t *store, md_store_group_t group, + const char *name, apr_pool_t *p) +{ + md_store_fs_t *s_fs = FS_STORE(store); + if (group == MD_SG_NONE) { + *pdname = s_fs->base; + return APR_SUCCESS; + } + return md_util_path_merge(pdname, p, s_fs->base, md_store_group_name(group), name, NULL); +} + +static void get_pass(const char **ppass, apr_size_t *plen, + md_store_fs_t *s_fs, md_store_group_t group) +{ + if (s_fs->plain_pkey[group]) { + *ppass = NULL; + *plen = 0; + } + else { + *ppass = (const char *)s_fs->key; + *plen = s_fs->key_len; + } +} + +static apr_status_t fs_fload(void **pvalue, md_store_fs_t *s_fs, const char *fpath, + md_store_group_t group, md_store_vtype_t vtype, + apr_pool_t *p, apr_pool_t *ptemp) +{ + apr_status_t rv; + const char *pass; + apr_size_t pass_len; + + if (pvalue != NULL) { + switch (vtype) { + case MD_SV_TEXT: + rv = md_text_fread8k((const char **)pvalue, p, fpath); + break; + case MD_SV_JSON: + rv = md_json_readf((md_json_t **)pvalue, p, fpath); + break; + case MD_SV_CERT: + rv = md_cert_fload((md_cert_t **)pvalue, p, fpath); + break; + case MD_SV_PKEY: + get_pass(&pass, &pass_len, s_fs, group); + rv = md_pkey_fload((md_pkey_t **)pvalue, p, pass, pass_len, fpath); + break; + case MD_SV_CHAIN: + rv = md_chain_fload((apr_array_header_t **)pvalue, p, fpath); + break; + default: + rv = APR_ENOTIMPL; + break; + } + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, ptemp, + "loading type %d from %s", vtype, fpath); + } + else { /* check for existence only */ + rv = md_util_is_file(fpath, p); + } + return rv; +} + +static apr_status_t pfs_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_fs_t *s_fs = baton; + const char *fpath, *name, *aspect; + md_store_vtype_t vtype; + md_store_group_t group; + void **pvalue; + apr_status_t rv; + MD_CHK_VARS; + + group = (md_store_group_t)va_arg(ap, int); + name = va_arg(ap, const char *); + aspect = va_arg(ap, const char *); + vtype = (md_store_vtype_t)va_arg(ap, int); + pvalue= va_arg(ap, void **); + + if (MD_OK(fs_get_fname(&fpath, &s_fs->s, group, name, aspect, ptemp))) { + rv = fs_fload(pvalue, s_fs, fpath, group, vtype, p, ptemp); + } + return rv; +} + +static apr_status_t dispatch(md_store_fs_t *s_fs, md_store_fs_ev_t ev, int group, + const char *fname, apr_filetype_e ftype, apr_pool_t *p) +{ + (void)ev; + if (s_fs->event_cb) { + return s_fs->event_cb(s_fs->event_baton, &s_fs->s, MD_S_FS_EV_CREATED, + group, fname, ftype, p); + } + return APR_SUCCESS; +} + +static apr_status_t mk_group_dir(const char **pdir, md_store_fs_t *s_fs, + md_store_group_t group, const char *name, + apr_pool_t *p) +{ + const perms_t *perms; + apr_status_t rv; + MD_CHK_VARS; + + perms = gperms(s_fs, group); + + if (MD_OK(fs_get_dname(pdir, &s_fs->s, group, name, p)) && (MD_SG_NONE != group)) { + if ( !MD_OK(md_util_is_dir(*pdir, p)) + && MD_OK(apr_dir_make_recursive(*pdir, perms->dir, p))) { + rv = dispatch(s_fs, MD_S_FS_EV_CREATED, group, *pdir, APR_DIR, p); + } + + if (APR_SUCCESS == rv) { + rv = apr_file_perms_set(*pdir, perms->dir); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "mk_group_dir %s perm set", *pdir); + if (APR_STATUS_IS_ENOTIMPL(rv)) { + rv = APR_SUCCESS; + } + } + } + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "mk_group_dir %d %s", group, name); + return rv; +} + +static apr_status_t pfs_is_newer(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_fs_t *s_fs = baton; + const char *fname1, *fname2, *name, *aspect; + md_store_group_t group1, group2; + apr_finfo_t inf1, inf2; + int *pnewer; + apr_status_t rv; + MD_CHK_VARS; + + (void)p; + group1 = (md_store_group_t)va_arg(ap, int); + group2 = (md_store_group_t)va_arg(ap, int); + name = va_arg(ap, const char*); + aspect = va_arg(ap, const char*); + pnewer = va_arg(ap, int*); + + *pnewer = 0; + if ( MD_OK(fs_get_fname(&fname1, &s_fs->s, group1, name, aspect, ptemp)) + && MD_OK(fs_get_fname(&fname2, &s_fs->s, group2, name, aspect, ptemp)) + && MD_OK(apr_stat(&inf1, fname1, APR_FINFO_MTIME, ptemp)) + && MD_OK(apr_stat(&inf2, fname2, APR_FINFO_MTIME, ptemp))) { + *pnewer = inf1.mtime > inf2.mtime; + } + + return rv; +} + + +static int fs_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2, + const char *name, const char *aspect, apr_pool_t *p) +{ + md_store_fs_t *s_fs = FS_STORE(store); + int newer = 0; + apr_status_t rv; + + rv = md_util_pool_vdo(pfs_is_newer, s_fs, p, group1, group2, name, aspect, &newer, NULL); + if (APR_SUCCESS == rv) { + return newer; + } + return 0; +} + +static apr_status_t pfs_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_fs_t *s_fs = baton; + const char *gdir, *dir, *fpath, *name, *aspect; + md_store_vtype_t vtype; + md_store_group_t group; + void *value; + int create; + apr_status_t rv; + const perms_t *perms; + const char *pass; + apr_size_t pass_len; + MD_CHK_VARS; + + group = (md_store_group_t)va_arg(ap, int); + name = va_arg(ap, const char*); + aspect = va_arg(ap, const char*); + vtype = (md_store_vtype_t)va_arg(ap, int); + value = va_arg(ap, void *); + create = va_arg(ap, int); + + perms = gperms(s_fs, group); + + if ( MD_OK(mk_group_dir(&gdir, s_fs, group, NULL, p)) + && MD_OK(mk_group_dir(&dir, s_fs, group, name, p)) + && MD_OK(md_util_path_merge(&fpath, ptemp, dir, aspect, NULL))) { + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "storing in %s", fpath); + switch (vtype) { + case MD_SV_TEXT: + rv = (create? md_text_fcreatex(fpath, perms->file, p, value) + : md_text_freplace(fpath, perms->file, p, value)); + break; + case MD_SV_JSON: + rv = (create? md_json_fcreatex((md_json_t *)value, p, MD_JSON_FMT_INDENT, + fpath, perms->file) + : md_json_freplace((md_json_t *)value, p, MD_JSON_FMT_INDENT, + fpath, perms->file)); + break; + case MD_SV_CERT: + rv = md_cert_fsave((md_cert_t *)value, ptemp, fpath, perms->file); + break; + case MD_SV_PKEY: + /* Take care that we write private key with access only to the user, + * unless we write the key encrypted */ + get_pass(&pass, &pass_len, s_fs, group); + rv = md_pkey_fsave((md_pkey_t *)value, ptemp, pass, pass_len, + fpath, (pass && pass_len)? perms->file : MD_FPROT_F_UONLY); + break; + case MD_SV_CHAIN: + rv = md_chain_fsave((apr_array_header_t*)value, ptemp, fpath, perms->file); + break; + default: + return APR_ENOTIMPL; + } + if (APR_SUCCESS == rv) { + rv = dispatch(s_fs, MD_S_FS_EV_CREATED, group, fpath, APR_REG, p); + } + } + return rv; +} + +static apr_status_t pfs_remove(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_fs_t *s_fs = baton; + const char *dir, *name, *fpath, *groupname, *aspect; + apr_status_t rv; + int force; + apr_finfo_t info; + md_store_group_t group; + MD_CHK_VARS; + + (void)p; + group = (md_store_group_t)va_arg(ap, int); + name = va_arg(ap, const char*); + aspect = va_arg(ap, const char *); + force = va_arg(ap, int); + + groupname = md_store_group_name(group); + + if ( MD_OK(md_util_path_merge(&dir, ptemp, s_fs->base, groupname, name, NULL)) + && MD_OK(md_util_path_merge(&fpath, ptemp, dir, aspect, NULL))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "start remove of md %s/%s/%s", + groupname, name, aspect); + + if (!MD_OK(apr_stat(&info, dir, APR_FINFO_TYPE, ptemp))) { + if (APR_ENOENT == rv && force) { + return APR_SUCCESS; + } + return rv; + } + + rv = apr_file_remove(fpath, ptemp); + if (APR_ENOENT == rv && force) { + rv = APR_SUCCESS; + } + } + return rv; +} + +static apr_status_t fs_load(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void **pvalue, apr_pool_t *p) +{ + md_store_fs_t *s_fs = FS_STORE(store); + return md_util_pool_vdo(pfs_load, s_fs, p, group, name, aspect, vtype, pvalue, NULL); +} + +static apr_status_t fs_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, const char *aspect, + md_store_vtype_t vtype, void *value, int create) +{ + md_store_fs_t *s_fs = FS_STORE(store); + return md_util_pool_vdo(pfs_save, s_fs, p, group, name, aspect, + vtype, value, create, NULL); +} + +static apr_status_t fs_remove(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, + apr_pool_t *p, int force) +{ + md_store_fs_t *s_fs = FS_STORE(store); + return md_util_pool_vdo(pfs_remove, s_fs, p, group, name, aspect, force, NULL); +} + +static apr_status_t pfs_purge(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_fs_t *s_fs = baton; + const char *dir, *name, *groupname; + md_store_group_t group; + apr_status_t rv; + MD_CHK_VARS; + + (void)p; + group = (md_store_group_t)va_arg(ap, int); + name = va_arg(ap, const char*); + + groupname = md_store_group_name(group); + + if (MD_OK(md_util_path_merge(&dir, ptemp, s_fs->base, groupname, name, NULL))) { + /* Remove all files in dir, there should be no sub-dirs */ + rv = md_util_rm_recursive(dir, ptemp, 1); + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "purge %s/%s (%s)", groupname, name, dir); + return APR_SUCCESS; +} + +static apr_status_t fs_purge(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name) +{ + md_store_fs_t *s_fs = FS_STORE(store); + return md_util_pool_vdo(pfs_purge, s_fs, p, group, name, NULL); +} + +/**************************************************************************************************/ +/* iteration */ + +typedef struct { + md_store_fs_t *s_fs; + md_store_group_t group; + const char *pattern; + const char *aspect; + md_store_vtype_t vtype; + md_store_inspect *inspect; + void *baton; +} inspect_ctx; + +static apr_status_t insp(void *baton, apr_pool_t *p, apr_pool_t *ptemp, + const char *dir, const char *name, apr_filetype_e ftype) +{ + inspect_ctx *ctx = baton; + apr_status_t rv; + void *value; + const char *fpath; + MD_CHK_VARS; + + (void)ftype; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "inspecting value at: %s/%s", dir, name); + if ( MD_OK(md_util_path_merge(&fpath, ptemp, dir, name, NULL)) + && MD_OK(fs_fload(&value, ctx->s_fs, fpath, ctx->group, ctx->vtype, p, ptemp)) + && !ctx->inspect(ctx->baton, name, ctx->aspect, ctx->vtype, value, ptemp)) { + return APR_EOF; + } + return rv; +} + +static apr_status_t fs_iterate(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern, + const char *aspect, md_store_vtype_t vtype) +{ + const char *groupname; + apr_status_t rv; + inspect_ctx ctx; + + ctx.s_fs = FS_STORE(store); + ctx.group = group; + ctx.pattern = pattern; + ctx.aspect = aspect; + ctx.vtype = vtype; + ctx.inspect = inspect; + ctx.baton = baton; + groupname = md_store_group_name(group); + + rv = md_util_files_do(insp, &ctx, p, ctx.s_fs->base, groupname, ctx.pattern, aspect, NULL); + + return rv; +} + +/**************************************************************************************************/ +/* moving */ + +static apr_status_t pfs_move(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_fs_t *s_fs = baton; + const char *name, *from_group, *to_group, *from_dir, *to_dir, *arch_dir, *dir; + md_store_group_t from, to; + int archive; + apr_status_t rv; + MD_CHK_VARS; + + (void)p; + from = (md_store_group_t)va_arg(ap, int); + to = (md_store_group_t)va_arg(ap, int); + name = va_arg(ap, const char*); + archive = va_arg(ap, int); + + from_group = md_store_group_name(from); + to_group = md_store_group_name(to); + if (!strcmp(from_group, to_group)) { + return APR_EINVAL; + } + + if ( !MD_OK(md_util_path_merge(&from_dir, ptemp, s_fs->base, from_group, name, NULL)) + || !MD_OK(md_util_path_merge(&to_dir, ptemp, s_fs->base, to_group, name, NULL))) { + goto out; + } + + if (!MD_OK(md_util_is_dir(from_dir, ptemp))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "source is no dir: %s", from_dir); + goto out; + } + + if (MD_OK(archive? md_util_is_dir(to_dir, ptemp) : APR_ENOENT)) { + int n = 1; + const char *narch_dir; + + if ( !MD_OK(md_util_path_merge(&dir, ptemp, s_fs->base, + md_store_group_name(MD_SG_ARCHIVE), NULL)) + || !MD_OK(apr_dir_make_recursive(dir, MD_FPROT_D_UONLY, ptemp)) + || !MD_OK(md_util_path_merge(&arch_dir, ptemp, dir, name, NULL))) { + goto out; + } + +#ifdef WIN32 + /* WIN32 and handling of files/dirs. What can one say? */ + + while (n < 1000) { + narch_dir = apr_psprintf(ptemp, "%s.%d", arch_dir, n); + rv = md_util_is_dir(narch_dir, ptemp); + if (APR_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "using archive dir: %s", + narch_dir); + break; + } + else { + ++n; + narch_dir = NULL; + } + } + +#else /* ifdef WIN32 */ + + while (n < 1000) { + narch_dir = apr_psprintf(ptemp, "%s.%d", arch_dir, n); + if (MD_OK(apr_dir_make(narch_dir, MD_FPROT_D_UONLY, ptemp))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "using archive dir: %s", + narch_dir); + break; + } + else if (APR_EEXIST == rv) { + ++n; + narch_dir = NULL; + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "creating archive dir: %s", + narch_dir); + goto out; + } + } + +#endif /* ifdef WIN32 (else part) */ + + if (!narch_dir) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "ran out of numbers less than 1000 " + "while looking for an available one in %s to archive the data " + "from %s. Either something is generally wrong or you need to " + "clean up some of those directories.", arch_dir, from_dir); + rv = APR_EGENERAL; + goto out; + } + + if (!MD_OK(apr_file_rename(to_dir, narch_dir, ptemp))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", + to_dir, narch_dir); + goto out; + } + if (!MD_OK(apr_file_rename(from_dir, to_dir, ptemp))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", + from_dir, to_dir); + apr_file_rename(narch_dir, to_dir, ptemp); + goto out; + } + if (MD_OK(dispatch(s_fs, MD_S_FS_EV_MOVED, to, to_dir, APR_DIR, ptemp))) { + rv = dispatch(s_fs, MD_S_FS_EV_MOVED, MD_SG_ARCHIVE, narch_dir, APR_DIR, ptemp); + } + } + else if (APR_STATUS_IS_ENOENT(rv)) { + if (APR_SUCCESS != (rv = apr_file_rename(from_dir, to_dir, ptemp))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", + from_dir, to_dir); + goto out; + } + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "target is no dir: %s", to_dir); + goto out; + } + +out: + return rv; +} + +static apr_status_t fs_move(md_store_t *store, apr_pool_t *p, + md_store_group_t from, md_store_group_t to, + const char *name, int archive) +{ + md_store_fs_t *s_fs = FS_STORE(store); + return md_util_pool_vdo(pfs_move, s_fs, p, from, to, name, archive, NULL); +} diff --git a/modules/md/md_store_fs.h b/modules/md/md_store_fs.h new file mode 100644 index 0000000..4167c9b --- /dev/null +++ b/modules/md/md_store_fs.h @@ -0,0 +1,65 @@ +/* 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. + */ + +#ifndef mod_md_md_store_fs_h +#define mod_md_md_store_fs_h + +struct md_store_t; + +/** + * Default file permissions set by the store, user only read/write(/exec), + * if so supported by the apr. + */ +#define MD_FPROT_F_UONLY (APR_FPROT_UREAD|APR_FPROT_UWRITE) +#define MD_FPROT_D_UONLY (MD_FPROT_F_UONLY|APR_FPROT_UEXECUTE) + +/** + * User has all permission, group can read, other none + */ +#define MD_FPROT_F_UALL_GREAD (MD_FPROT_F_UONLY|APR_FPROT_GREAD) +#define MD_FPROT_D_UALL_GREAD (MD_FPROT_D_UONLY|APR_FPROT_GREAD|APR_FPROT_GEXECUTE) + +/** + * User has all permission, group and others can read + */ +#define MD_FPROT_F_UALL_WREAD (MD_FPROT_F_UALL_GREAD|APR_FPROT_WREAD) +#define MD_FPROT_D_UALL_WREAD (MD_FPROT_D_UALL_GREAD|APR_FPROT_WREAD|APR_FPROT_WEXECUTE) + +apr_status_t md_store_fs_init(struct md_store_t **pstore, apr_pool_t *p, + const char *path); + + +apr_status_t md_store_fs_default_perms_set(struct md_store_t *store, + apr_fileperms_t file_perms, + apr_fileperms_t dir_perms); +apr_status_t md_store_fs_group_perms_set(struct md_store_t *store, + md_store_group_t group, + apr_fileperms_t file_perms, + apr_fileperms_t dir_perms); + +typedef enum { + MD_S_FS_EV_CREATED, + MD_S_FS_EV_MOVED, +} md_store_fs_ev_t; + +typedef apr_status_t md_store_fs_cb(void *baton, struct md_store_t *store, + md_store_fs_ev_t ev, int group, + const char *fname, apr_filetype_e ftype, + apr_pool_t *p); + +apr_status_t md_store_fs_set_event_cb(struct md_store_t *store, md_store_fs_cb *cb, void *baton); + +#endif /* mod_md_md_store_fs_h */ diff --git a/modules/md/md_util.c b/modules/md/md_util.c new file mode 100644 index 0000000..4e97d92 --- /dev/null +++ b/modules/md/md_util.c @@ -0,0 +1,1254 @@ +/* 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. + */ + +#include <stdio.h> + +#include <apr_lib.h> +#include <apr_strings.h> +#include <apr_portable.h> +#include <apr_file_info.h> +#include <apr_fnmatch.h> +#include <apr_tables.h> +#include <apr_uri.h> + +#include "md_log.h" +#include "md_util.h" + +/**************************************************************************************************/ +/* pool utils */ + +apr_status_t md_util_pool_do(md_util_action *cb, void *baton, apr_pool_t *p) +{ + apr_pool_t *ptemp; + apr_status_t rv = apr_pool_create(&ptemp, p); + if (APR_SUCCESS == rv) { + rv = cb(baton, p, ptemp); + + apr_pool_destroy(ptemp); + } + return rv; +} + +static apr_status_t pool_vado(md_util_vaction *cb, void *baton, apr_pool_t *p, va_list ap) +{ + apr_pool_t *ptemp; + apr_status_t rv; + + rv = apr_pool_create(&ptemp, p); + if (APR_SUCCESS == rv) { + rv = cb(baton, p, ptemp, ap); + apr_pool_destroy(ptemp); + } + return rv; +} + +apr_status_t md_util_pool_vdo(md_util_vaction *cb, void *baton, apr_pool_t *p, ...) +{ + va_list ap; + apr_status_t rv; + + va_start(ap, p); + rv = pool_vado(cb, baton, p, ap); + va_end(ap); + return rv; +} + +/**************************************************************************************************/ +/* string related */ + +char *md_util_str_tolower(char *s) +{ + char *orig = s; + while (*s) { + *s = (char)apr_tolower(*s); + ++s; + } + return orig; +} + +int md_array_str_index(const apr_array_header_t *array, const char *s, + int start, int case_sensitive) +{ + if (start >= 0) { + int i; + + for (i = start; i < array->nelts; i++) { + const char *p = APR_ARRAY_IDX(array, i, const char *); + if ((case_sensitive && !strcmp(p, s)) + || (!case_sensitive && !apr_strnatcasecmp(p, s))) { + return i; + } + } + } + + return -1; +} + +int md_array_str_eq(const struct apr_array_header_t *a1, + const struct apr_array_header_t *a2, int case_sensitive) +{ + int i; + const char *s1, *s2; + + if (a1 == a2) return 1; + if (!a1) return 0; + if (a1->nelts != a2->nelts) return 0; + for (i = 0; i < a1->nelts; ++i) { + s1 = APR_ARRAY_IDX(a1, i, const char *); + s2 = APR_ARRAY_IDX(a2, i, const char *); + if ((case_sensitive && strcmp(s1, s2)) + || (!case_sensitive && apr_strnatcasecmp(s1, s2))) { + return 0; + } + } + return 1; +} + +apr_array_header_t *md_array_str_clone(apr_pool_t *p, apr_array_header_t *src) +{ + apr_array_header_t *dest = apr_array_make(p, src->nelts, sizeof(const char*)); + if (dest) { + int i; + for (i = 0; i < src->nelts; i++) { + const char *s = APR_ARRAY_IDX(src, i, const char *); + APR_ARRAY_PUSH(dest, const char *) = apr_pstrdup(p, s); + } + } + return dest; +} + +struct apr_array_header_t *md_array_str_compact(apr_pool_t *p, struct apr_array_header_t *src, + int case_sensitive) +{ + apr_array_header_t *dest = apr_array_make(p, src->nelts, sizeof(const char*)); + if (dest) { + const char *s; + int i; + for (i = 0; i < src->nelts; ++i) { + s = APR_ARRAY_IDX(src, i, const char *); + if (md_array_str_index(dest, s, 0, case_sensitive) < 0) { + APR_ARRAY_PUSH(dest, char *) = md_util_str_tolower(apr_pstrdup(p, s)); + } + } + } + return dest; +} + +apr_array_header_t *md_array_str_remove(apr_pool_t *p, apr_array_header_t *src, + const char *exclude, int case_sensitive) +{ + apr_array_header_t *dest = apr_array_make(p, src->nelts, sizeof(const char*)); + if (dest) { + int i; + for (i = 0; i < src->nelts; i++) { + const char *s = APR_ARRAY_IDX(src, i, const char *); + if (!exclude + || (case_sensitive && strcmp(exclude, s)) + || (!case_sensitive && apr_strnatcasecmp(exclude, s))) { + APR_ARRAY_PUSH(dest, const char *) = apr_pstrdup(p, s); + } + } + } + return dest; +} + +int md_array_str_add_missing(apr_array_header_t *dest, apr_array_header_t *src, int case_sensitive) +{ + int i, added = 0; + for (i = 0; i < src->nelts; i++) { + const char *s = APR_ARRAY_IDX(src, i, const char *); + if (md_array_str_index(dest, s, 0, case_sensitive) < 0) { + APR_ARRAY_PUSH(dest, const char *) = s; + ++added; + } + } + return added; +} + +/**************************************************************************************************/ +/* file system related */ + +apr_status_t md_util_fopen(FILE **pf, const char *fn, const char *mode) +{ + *pf = fopen(fn, mode); + if (*pf == NULL) { + return errno; + } + + return APR_SUCCESS; +} + +apr_status_t md_util_fcreatex(apr_file_t **pf, const char *fn, + apr_fileperms_t perms, apr_pool_t *p) +{ + return apr_file_open(pf, fn, (APR_FOPEN_WRITE|APR_FOPEN_CREATE|APR_FOPEN_EXCL), + perms, p); +} + +apr_status_t md_util_is_dir(const char *path, apr_pool_t *pool) +{ + apr_finfo_t info; + apr_status_t rv = apr_stat(&info, path, APR_FINFO_TYPE, pool); + if (rv == APR_SUCCESS) { + rv = (info.filetype == APR_DIR)? APR_SUCCESS : APR_EINVAL; + } + return rv; +} + +apr_status_t md_util_is_file(const char *path, apr_pool_t *pool) +{ + apr_finfo_t info; + apr_status_t rv = apr_stat(&info, path, APR_FINFO_TYPE, pool); + if (rv == APR_SUCCESS) { + rv = (info.filetype == APR_REG)? APR_SUCCESS : APR_EINVAL; + } + return rv; +} + +apr_status_t md_util_path_merge(const char **ppath, apr_pool_t *p, ...) +{ + const char *segment, *path; + va_list ap; + apr_status_t rv = APR_SUCCESS; + + va_start(ap, p); + path = va_arg(ap, char *); + while (path && APR_SUCCESS == rv && (segment = va_arg(ap, char *))) { + rv = apr_filepath_merge((char **)&path, path, segment, APR_FILEPATH_SECUREROOT , p); + } + va_end(ap); + + *ppath = (APR_SUCCESS == rv)? (path? path : "") : NULL; + return rv; +} + +apr_status_t md_util_freplace(const char *fpath, apr_fileperms_t perms, apr_pool_t *p, + md_util_file_cb *write_cb, void *baton) +{ + apr_status_t rv = APR_EEXIST; + apr_file_t *f; + const char *tmp; + int i, max; + + tmp = apr_psprintf(p, "%s.tmp", fpath); + i = 0; max = 20; +creat: + while (i < max && APR_EEXIST == (rv = md_util_fcreatex(&f, tmp, perms, p))) { + ++i; + apr_sleep(apr_time_msec(50)); + } + if (APR_EEXIST == rv + && APR_SUCCESS == (rv = apr_file_remove(tmp, p)) + && max <= 20) { + max *= 2; + goto creat; + } + + if (APR_SUCCESS == rv) { + rv = write_cb(baton, f, p); + apr_file_close(f); + + if (APR_SUCCESS == rv) { + rv = apr_file_rename(tmp, fpath, p); + if (APR_SUCCESS != rv) { + apr_file_remove(tmp, p); + } + } + } + return rv; +} + +/**************************************************************************************************/ +/* text files */ + +apr_status_t md_text_fread8k(const char **ptext, apr_pool_t *p, const char *fpath) +{ + apr_status_t rv; + apr_file_t *f; + char buffer[8 * 1024]; + + *ptext = NULL; + if (APR_SUCCESS == (rv = apr_file_open(&f, fpath, APR_FOPEN_READ, 0, p))) { + apr_size_t blen = sizeof(buffer)/sizeof(buffer[0]) - 1; + rv = apr_file_read_full(f, buffer, blen, &blen); + if (APR_SUCCESS == rv || APR_STATUS_IS_EOF(rv)) { + *ptext = apr_pstrndup(p, buffer, blen); + rv = APR_SUCCESS; + } + apr_file_close(f); + } + return rv; +} + +static apr_status_t write_text(void *baton, struct apr_file_t *f, apr_pool_t *p) +{ + const char *text = baton; + apr_size_t len = strlen(text); + + (void)p; + return apr_file_write_full(f, text, len, &len); +} + +apr_status_t md_text_fcreatex(const char *fpath, apr_fileperms_t perms, + apr_pool_t *p, const char *text) +{ + apr_status_t rv; + apr_file_t *f; + + rv = md_util_fcreatex(&f, fpath, perms, p); + if (APR_SUCCESS == rv) { + rv = write_text((void*)text, f, p); + apr_file_close(f); + } + return rv; +} + +apr_status_t md_text_freplace(const char *fpath, apr_fileperms_t perms, + apr_pool_t *p, const char *text) +{ + return md_util_freplace(fpath, perms, p, write_text, (void*)text); +} + +typedef struct { + const char *path; + apr_array_header_t *patterns; + int follow_links; + void *baton; + md_util_fdo_cb *cb; +} md_util_fwalk_t; + +static apr_status_t rm_recursive(const char *fpath, apr_pool_t *p, int max_level) +{ + apr_finfo_t info; + apr_status_t rv; + const char *npath; + + if (APR_SUCCESS != (rv = apr_stat(&info, fpath, (APR_FINFO_TYPE|APR_FINFO_LINK), p))) { + return rv; + } + + if (info.filetype == APR_DIR) { + if (max_level > 0) { + apr_dir_t *d; + + if (APR_SUCCESS == (rv = apr_dir_open(&d, fpath, p))) { + + while (APR_SUCCESS == rv && + APR_SUCCESS == (rv = apr_dir_read(&info, APR_FINFO_TYPE, d))) { + if (!strcmp(".", info.name) || !strcmp("..", info.name)) { + continue; + } + + rv = md_util_path_merge(&npath, p, fpath, info.name, NULL); + if (APR_SUCCESS == rv) { + rv = rm_recursive(npath, p, max_level - 1); + } + } + apr_dir_close(d); + if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + } + } + } + if (APR_SUCCESS == rv) { + rv = apr_dir_remove(fpath, p); + } + } + else { + rv = apr_file_remove(fpath, p); + } + return rv; +} + +static apr_status_t prm_recursive(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + int max_level = va_arg(ap, int); + + (void)p; + return rm_recursive(baton, ptemp, max_level); +} + +apr_status_t md_util_rm_recursive(const char *fpath, apr_pool_t *p, int max_level) +{ + return md_util_pool_vdo(prm_recursive, (void*)fpath, p, max_level, NULL); +} + +static apr_status_t match_and_do(md_util_fwalk_t *ctx, const char *path, int depth, + apr_pool_t *p, apr_pool_t *ptemp) +{ + apr_status_t rv = APR_SUCCESS; + const char *pattern, *npath; + apr_dir_t *d; + apr_finfo_t finfo; + int ndepth = depth + 1; + apr_int32_t wanted = (APR_FINFO_TYPE); + + if (depth >= ctx->patterns->nelts) { + return APR_SUCCESS; + } + pattern = APR_ARRAY_IDX(ctx->patterns, depth, const char *); + + rv = apr_dir_open(&d, path, ptemp); + if (APR_SUCCESS != rv) { + return rv; + } + + while (APR_SUCCESS == (rv = apr_dir_read(&finfo, wanted, d))) { + if (!strcmp(".", finfo.name) || !strcmp("..", finfo.name)) { + continue; + } + if (APR_SUCCESS == apr_fnmatch(pattern, finfo.name, 0)) { + if (ndepth < ctx->patterns->nelts) { + if (APR_DIR == finfo.filetype) { + /* deeper and deeper, irgendwo in der tiefe leuchtet ein licht */ + rv = md_util_path_merge(&npath, ptemp, path, finfo.name, NULL); + if (APR_SUCCESS == rv) { + rv = match_and_do(ctx, npath, ndepth, p, ptemp); + } + } + } + else { + rv = ctx->cb(ctx->baton, p, ptemp, path, finfo.name, finfo.filetype); + } + } + if (APR_SUCCESS != rv) { + break; + } + } + + if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + } + + apr_dir_close(d); + return rv; +} + +static apr_status_t files_do_start(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_util_fwalk_t *ctx = baton; + const char *segment; + + ctx->patterns = apr_array_make(ptemp, 5, sizeof(const char*)); + + segment = va_arg(ap, char *); + while (segment) { + APR_ARRAY_PUSH(ctx->patterns, const char *) = segment; + segment = va_arg(ap, char *); + } + + return match_and_do(ctx, ctx->path, 0, p, ptemp); +} + +apr_status_t md_util_files_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p, + const char *path, ...) +{ + apr_status_t rv; + va_list ap; + md_util_fwalk_t ctx; + + memset(&ctx, 0, sizeof(ctx)); + ctx.path = path; + ctx.follow_links = 1; + ctx.cb = cb; + ctx.baton = baton; + + va_start(ap, path); + rv = pool_vado(files_do_start, &ctx, p, ap); + va_end(ap); + + return rv; +} + +static apr_status_t tree_do(void *baton, apr_pool_t *p, apr_pool_t *ptemp, const char *path) +{ + md_util_fwalk_t *ctx = baton; + + apr_status_t rv = APR_SUCCESS; + const char *name, *fpath; + apr_filetype_e ftype; + apr_dir_t *d; + apr_int32_t wanted = APR_FINFO_TYPE; + apr_finfo_t finfo; + + if (APR_SUCCESS == (rv = apr_dir_open(&d, path, ptemp))) { + while (APR_SUCCESS == (rv = apr_dir_read(&finfo, wanted, d))) { + name = finfo.name; + if (!strcmp(".", name) || !strcmp("..", name)) { + continue; + } + + fpath = NULL; + ftype = finfo.filetype; + + if (APR_LNK == ftype && ctx->follow_links) { + rv = md_util_path_merge(&fpath, ptemp, path, name, NULL); + if (APR_SUCCESS == rv) { + rv = apr_stat(&finfo, ctx->path, wanted, ptemp); + } + } + + if (APR_DIR == finfo.filetype) { + if (!fpath) { + rv = md_util_path_merge(&fpath, ptemp, path, name, NULL); + } + if (APR_SUCCESS == rv) { + rv = tree_do(ctx, p, ptemp, fpath); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, ptemp, "dir cb(%s/%s)", + path, name); + rv = ctx->cb(ctx->baton, p, ptemp, path, name, ftype); + } + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, ptemp, "file cb(%s/%s)", + path, name); + rv = ctx->cb(ctx->baton, p, ptemp, path, name, finfo.filetype); + } + } + + apr_dir_close(d); + + if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + } + } + return rv; +} + +static apr_status_t tree_start_do(void *baton, apr_pool_t *p, apr_pool_t *ptemp) +{ + md_util_fwalk_t *ctx = baton; + apr_finfo_t info; + apr_status_t rv; + apr_int32_t wanted = ctx->follow_links? APR_FINFO_TYPE : (APR_FINFO_TYPE|APR_FINFO_LINK); + + rv = apr_stat(&info, ctx->path, wanted, ptemp); + if (rv == APR_SUCCESS) { + switch (info.filetype) { + case APR_DIR: + rv = tree_do(ctx, p, ptemp, ctx->path); + break; + default: + rv = APR_EINVAL; + } + } + return rv; +} + +apr_status_t md_util_tree_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p, + const char *path, int follow_links) +{ + apr_status_t rv; + md_util_fwalk_t ctx; + + memset(&ctx, 0, sizeof(ctx)); + ctx.path = path; + ctx.follow_links = follow_links; + ctx.cb = cb; + ctx.baton = baton; + + rv = md_util_pool_do(tree_start_do, &ctx, p); + + return rv; +} + +static apr_status_t rm_cb(void *baton, apr_pool_t *p, apr_pool_t *ptemp, + const char *path, const char *name, apr_filetype_e ftype) +{ + apr_status_t rv; + const char *fpath; + + (void)baton; + (void)p; + rv = md_util_path_merge(&fpath, ptemp, path, name, NULL); + if (APR_SUCCESS == rv) { + if (APR_DIR == ftype) { + rv = apr_dir_remove(fpath, ptemp); + } + else { + rv = apr_file_remove(fpath, ptemp); + } + } + return rv; +} + +apr_status_t md_util_ftree_remove(const char *path, apr_pool_t *p) +{ + apr_status_t rv = md_util_tree_do(rm_cb, NULL, p, path, 0); + if (APR_SUCCESS == rv) { + rv = apr_dir_remove(path, p); + } + return rv; +} + +/* DNS name checks ********************************************************************************/ + +int md_util_is_dns_name(apr_pool_t *p, const char *hostname, int need_fqdn) +{ + char c, last = 0; + const char *cp = hostname; + int dots = 0; + + /* Since we use the names in certificates, we need pure ASCII domain names + * and IDN need to be converted to unicode. */ + while ((c = *cp++)) { + switch (c) { + case '.': + if (last == '.') { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "dns name with ..: %s", + hostname); + return 0; + } + ++dots; + break; + case '-': + break; + default: + if (!apr_isalnum(c)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "dns invalid char %c: %s", + c, hostname); + return 0; + } + break; + } + last = c; + } + + if (last == '.') { /* DNS names may end with '.' */ + --dots; + } + if (need_fqdn && dots <= 0) { /* do not accept just top level domains */ + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "not a FQDN: %s", hostname); + return 0; + } + return 1; /* empty string not allowed */ +} + +const char *md_util_schemify(apr_pool_t *p, const char *s, const char *def_scheme) +{ + const char *cp = s; + while (*cp) { + if (*cp == ':') { + /* could be an url scheme, leave unchanged */ + return s; + } + else if (!apr_isalnum(*cp)) { + break; + } + ++cp; + } + return apr_psprintf(p, "%s:%s", def_scheme, s); +} + +static apr_status_t uri_check(apr_uri_t *uri_parsed, apr_pool_t *p, + const char *uri, const char **perr) +{ + const char *s, *err = NULL; + apr_status_t rv; + + if (APR_SUCCESS != (rv = apr_uri_parse(p, uri, uri_parsed))) { + err = "not an uri"; + } + else if (uri_parsed->scheme) { + if (strlen(uri_parsed->scheme) + 1 >= strlen(uri)) { + err = "missing uri identifier"; + } + else if (!strncmp("http", uri_parsed->scheme, 4)) { + if (!uri_parsed->hostname) { + err = "missing hostname"; + } + else if (!md_util_is_dns_name(p, uri_parsed->hostname, 0)) { + err = "invalid hostname"; + } + if (uri_parsed->port_str + && (!apr_isdigit(uri_parsed->port_str[0]) + || uri_parsed->port == 0 + || uri_parsed->port > 65353)) { + err = "invalid port"; + } + } + else if (!strcmp("mailto", uri_parsed->scheme)) { + s = strchr(uri, '@'); + if (!s) { + err = "missing @"; + } + else if (strchr(s+1, '@')) { + err = "duplicate @"; + } + else if (s == uri + strlen(uri_parsed->scheme) + 1) { + err = "missing local part"; + } + else if (s == (uri + strlen(uri)-1)) { + err = "missing hostname"; + } + else if (strstr(uri, "..")) { + err = "double period"; + } + } + } + if (strchr(uri, ' ') || strchr(uri, '\t') ) { + err = "whitespace in uri"; + } + + if (err) { + rv = APR_EINVAL; + } + *perr = err; + return rv; +} + +apr_status_t md_util_abs_uri_check(apr_pool_t *p, const char *uri, const char **perr) +{ + apr_uri_t uri_parsed; + apr_status_t rv; + + if (APR_SUCCESS == (rv = uri_check(&uri_parsed, p, uri, perr))) { + if (!uri_parsed.scheme) { + *perr = "missing uri scheme"; + return APR_EINVAL; + } + } + return rv; +} + +apr_status_t md_util_abs_http_uri_check(apr_pool_t *p, const char *uri, const char **perr) +{ + apr_uri_t uri_parsed; + apr_status_t rv; + + if (APR_SUCCESS == (rv = uri_check(&uri_parsed, p, uri, perr))) { + if (!uri_parsed.scheme) { + *perr = "missing uri scheme"; + return APR_EINVAL; + } + if (apr_strnatcasecmp("http", uri_parsed.scheme) + && apr_strnatcasecmp("https", uri_parsed.scheme)) { + *perr = "uri scheme must be http or https"; + return APR_EINVAL; + } + } + return rv; +} + +/* try and retry for a while **********************************************************************/ + +apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs, + apr_interval_time_t timeout, apr_interval_time_t start_delay, + apr_interval_time_t max_delay, int backoff) +{ + apr_status_t rv; + apr_time_t now = apr_time_now(); + apr_time_t giveup = now + timeout; + apr_interval_time_t nap_duration = start_delay? start_delay : apr_time_from_msec(100); + apr_interval_time_t nap_max = max_delay? max_delay : apr_time_from_sec(10); + apr_interval_time_t left; + int i = 0; + + while (1) { + if (APR_SUCCESS == (rv = fn(baton, i++))) { + break; + } + else if (!APR_STATUS_IS_EAGAIN(rv) && !ignore_errs) { + break; + } + + now = apr_time_now(); + if (now > giveup) { + rv = APR_TIMEUP; + break; + } + + left = giveup - now; + if (nap_duration > left) { + nap_duration = left; + } + if (nap_duration > nap_max) { + nap_duration = nap_max; + } + + apr_sleep(nap_duration); + if (backoff) { + nap_duration *= 2; + } + } + return rv; +} + +/* execute process ********************************************************************************/ + +apr_status_t md_util_exec(apr_pool_t *p, const char *cmd, const char * const *argv, + int *exit_code) +{ + apr_status_t rv; + apr_procattr_t *procattr; + apr_proc_t *proc; + apr_exit_why_e ewhy; + + *exit_code = 0; + if (!(proc = apr_pcalloc(p, sizeof(*proc)))) { + return APR_ENOMEM; + } + if ( APR_SUCCESS == (rv = apr_procattr_create(&procattr, p)) + && APR_SUCCESS == (rv = apr_procattr_io_set(procattr, APR_NO_FILE, + APR_NO_PIPE, APR_NO_PIPE)) + && APR_SUCCESS == (rv = apr_procattr_cmdtype_set(procattr, APR_PROGRAM)) + && APR_SUCCESS == (rv = apr_proc_create(proc, cmd, argv, NULL, procattr, p)) + && APR_CHILD_DONE == (rv = apr_proc_wait(proc, exit_code, &ewhy, APR_WAIT))) { + /* let's not dwell on exit stati, but core should signal something's bad */ + if (*exit_code > 127 || APR_PROC_SIGNAL_CORE == ewhy) { + return APR_EINCOMPLETE; + } + return APR_SUCCESS; + } + return rv; +} + + +/* date/time encoding *****************************************************************************/ + +const char *md_print_duration(apr_pool_t *p, apr_interval_time_t duration) +{ + int secs = (int)(apr_time_sec(duration) % MD_SECS_PER_DAY); + return apr_psprintf(p, "%2d:%02d:%02d hours", + (int)secs/MD_SECS_PER_HOUR, (int)(secs%(MD_SECS_PER_HOUR))/60, + (int)(secs%60)); +} + + +/* base64 url encoding ****************************************************************************/ + +#define N6 (unsigned int)-1 + +static const unsigned int BASE64URL_UINT6[] = { +/* 0 1 2 3 4 5 6 7 8 9 a b c d e f */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* 0 */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* 1 */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, 62, N6, N6, /* 2 */ + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, N6, N6, N6, N6, N6, N6, /* 3 */ + N6, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 4 */ + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, N6, N6, N6, N6, 63, /* 5 */ + N6, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 6 */ + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, N6, N6, N6, N6, N6, /* 7 */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* 8 */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* 9 */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* a */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* b */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* c */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* d */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /* e */ + N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6 /* f */ +}; +static const unsigned char BASE64URL_CHARS[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', /* 0 - 9 */ + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', /* 10 - 19 */ + 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', /* 20 - 29 */ + 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', /* 30 - 39 */ + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', /* 40 - 49 */ + 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', /* 50 - 59 */ + '8', '9', '-', '_', ' ', ' ', ' ', ' ', ' ', ' ', /* 60 - 69 */ +}; + +#define BASE64URL_CHAR(x) BASE64URL_CHARS[ (unsigned int)(x) & 0x3fu ] + +apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, + apr_pool_t *pool) +{ + const unsigned char *e = (const unsigned char *)encoded; + const unsigned char *p = e; + unsigned char *d; + unsigned int n; + long len, mlen, remain, i; + + while (*p && BASE64URL_UINT6[ *p ] != N6) { + ++p; + } + len = (int)(p - e); + mlen = (len/4)*4; + *decoded = apr_pcalloc(pool, (apr_size_t)len + 1); + + i = 0; + d = (unsigned char*)*decoded; + for (; i < mlen; i += 4) { + n = ((BASE64URL_UINT6[ e[i+0] ] << 18) + + (BASE64URL_UINT6[ e[i+1] ] << 12) + + (BASE64URL_UINT6[ e[i+2] ] << 6) + + (BASE64URL_UINT6[ e[i+3] ])); + *d++ = (unsigned char)(n >> 16); + *d++ = (unsigned char)(n >> 8 & 0xffu); + *d++ = (unsigned char)(n & 0xffu); + } + remain = len - mlen; + switch (remain) { + case 2: + n = ((BASE64URL_UINT6[ e[mlen+0] ] << 18) + + (BASE64URL_UINT6[ e[mlen+1] ] << 12)); + *d++ = (unsigned char)(n >> 16); + remain = 1; + break; + case 3: + n = ((BASE64URL_UINT6[ e[mlen+0] ] << 18) + + (BASE64URL_UINT6[ e[mlen+1] ] << 12) + + (BASE64URL_UINT6[ e[mlen+2] ] << 6)); + *d++ = (unsigned char)(n >> 16); + *d++ = (unsigned char)(n >> 8 & 0xffu); + remain = 2; + break; + default: /* do nothing */ + break; + } + return (apr_size_t)(mlen/4*3 + remain); +} + +const char *md_util_base64url_encode(const char *data, apr_size_t dlen, apr_pool_t *pool) +{ + int i, len = (int)dlen; + apr_size_t slen = ((dlen+2)/3)*4 + 1; /* 0 terminated */ + const unsigned char *udata = (const unsigned char*)data; + unsigned char *enc, *p = apr_pcalloc(pool, slen); + + enc = p; + for (i = 0; i < len-2; i+= 3) { + *p++ = BASE64URL_CHAR( (udata[i] >> 2) ); + *p++ = BASE64URL_CHAR( (udata[i] << 4) + (udata[i+1] >> 4) ); + *p++ = BASE64URL_CHAR( (udata[i+1] << 2) + (udata[i+2] >> 6) ); + *p++ = BASE64URL_CHAR( (udata[i+2]) ); + } + + if (i < len) { + *p++ = BASE64URL_CHAR( (udata[i] >> 2) ); + if (i == (len - 1)) { + *p++ = BASE64URL_CHARS[ ((unsigned int)udata[i] << 4) & 0x3fu ]; + } + else { + *p++ = BASE64URL_CHAR( (udata[i] << 4) + (udata[i+1] >> 4) ); + *p++ = BASE64URL_CHAR( (udata[i+1] << 2) ); + } + } + *p++ = '\0'; + return (char *)enc; +} + +/******************************************************************************* + * link header handling + ******************************************************************************/ + +typedef struct { + const char *s; + apr_size_t slen; + apr_size_t i; + apr_size_t link_start; + apr_size_t link_len; + apr_size_t pn_start; + apr_size_t pn_len; + apr_size_t pv_start; + apr_size_t pv_len; +} link_ctx; + +static int attr_char(char c) +{ + switch (c) { + case '!': + case '#': + case '$': + case '&': + case '+': + case '-': + case '.': + case '^': + case '_': + case '`': + case '|': + case '~': + return 1; + default: + return apr_isalnum(c); + } +} + +static int ptoken_char(char c) +{ + switch (c) { + case '!': + case '#': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case '-': + case '.': + case '/': + case ':': + case '<': + case '=': + case '>': + case '?': + case '@': + case '[': + case ']': + case '^': + case '_': + case '`': + case '{': + case '|': + case '}': + case '~': + return 1; + default: + return apr_isalnum(c); + } +} + +static int skip_ws(link_ctx *ctx) +{ + char c; + while (ctx->i < ctx->slen + && (((c = ctx->s[ctx->i]) == ' ') || (c == '\t'))) { + ++ctx->i; + } + return (ctx->i < ctx->slen); +} + +static int skip_nonws(link_ctx *ctx) +{ + char c; + while (ctx->i < ctx->slen + && (((c = ctx->s[ctx->i]) != ' ') && (c != '\t'))) { + ++ctx->i; + } + return (ctx->i < ctx->slen); +} + +static unsigned int find_chr(link_ctx *ctx, char c, apr_size_t *pidx) +{ + apr_size_t j; + for (j = ctx->i; j < ctx->slen; ++j) { + if (ctx->s[j] == c) { + *pidx = j; + return 1; + } + } + return 0; +} + +static int read_chr(link_ctx *ctx, char c) +{ + if (ctx->i < ctx->slen && ctx->s[ctx->i] == c) { + ++ctx->i; + return 1; + } + return 0; +} + +static int skip_qstring(link_ctx *ctx) +{ + if (skip_ws(ctx) && read_chr(ctx, '\"')) { + apr_size_t end; + if (find_chr(ctx, '\"', &end)) { + ctx->i = end + 1; + return 1; + } + } + return 0; +} + +static int skip_ptoken(link_ctx *ctx) +{ + if (skip_ws(ctx)) { + apr_size_t i; + for (i = ctx->i; i < ctx->slen && ptoken_char(ctx->s[i]); ++i) { + /* nop */ + } + if (i > ctx->i) { + ctx->i = i; + return 1; + } + } + return 0; +} + + +static int read_link(link_ctx *ctx) +{ + ctx->link_start = ctx->link_len = 0; + if (skip_ws(ctx) && read_chr(ctx, '<')) { + apr_size_t end; + if (find_chr(ctx, '>', &end)) { + ctx->link_start = ctx->i; + ctx->link_len = end - ctx->link_start; + ctx->i = end + 1; + return 1; + } + } + return 0; +} + +static int skip_pname(link_ctx *ctx) +{ + if (skip_ws(ctx)) { + apr_size_t i; + for (i = ctx->i; i < ctx->slen && attr_char(ctx->s[i]); ++i) { + /* nop */ + } + if (i > ctx->i) { + ctx->i = i; + return 1; + } + } + return 0; +} + +static int skip_pvalue(link_ctx *ctx) +{ + if (skip_ws(ctx) && read_chr(ctx, '=')) { + ctx->pv_start = ctx->i; + if (skip_qstring(ctx) || skip_ptoken(ctx)) { + ctx->pv_len = ctx->i - ctx->pv_start; + return 1; + } + } + return 0; +} + +static int skip_param(link_ctx *ctx) +{ + if (skip_ws(ctx) && read_chr(ctx, ';')) { + ctx->pn_start = ctx->i; + ctx->pn_len = 0; + if (skip_pname(ctx)) { + ctx->pn_len = ctx->i - ctx->pn_start; + ctx->pv_len = 0; + skip_pvalue(ctx); /* value is optional */ + return 1; + } + } + return 0; +} + +static int pv_contains(link_ctx *ctx, const char *s) +{ + apr_size_t pvstart = ctx->pv_start; + apr_size_t pvlen = ctx->pv_len; + + if (ctx->s[pvstart] == '\"' && pvlen > 1) { + ++pvstart; + pvlen -= 2; + } + if (pvlen > 0) { + apr_size_t slen = strlen(s); + link_ctx pvctx; + apr_size_t i; + + memset(&pvctx, 0, sizeof(pvctx)); + pvctx.s = ctx->s + pvstart; + pvctx.slen = pvlen; + + for (i = 0; i < pvctx.slen; i = pvctx.i) { + skip_nonws(&pvctx); + if ((pvctx.i - i) == slen && !strncmp(s, pvctx.s + i, slen)) { + return 1; + } + skip_ws(&pvctx); + } + } + return 0; +} + +/* RFC 5988 <https://tools.ietf.org/html/rfc5988#section-6.2.1> + Link = "Link" ":" #link-value + link-value = "<" URI-Reference ">" *( ";" link-param ) + link-param = ( ( "rel" "=" relation-types ) + | ( "anchor" "=" <"> URI-Reference <"> ) + | ( "rev" "=" relation-types ) + | ( "hreflang" "=" Language-Tag ) + | ( "media" "=" ( MediaDesc | ( <"> MediaDesc <"> ) ) ) + | ( "title" "=" quoted-string ) + | ( "title*" "=" ext-value ) + | ( "type" "=" ( media-type | quoted-mt ) ) + | ( link-extension ) ) + link-extension = ( parmname [ "=" ( ptoken | quoted-string ) ] ) + | ( ext-name-star "=" ext-value ) + ext-name-star = parmname "*" ; reserved for RFC2231-profiled + ; extensions. Whitespace NOT + ; allowed in between. + ptoken = 1*ptokenchar + ptokenchar = "!" | "#" | "$" | "%" | "&" | "'" | "(" + | ")" | "*" | "+" | "-" | "." | "/" | DIGIT + | ":" | "<" | "=" | ">" | "?" | "@" | ALPHA + | "[" | "]" | "^" | "_" | "`" | "{" | "|" + | "}" | "~" + media-type = type-name "/" subtype-name + quoted-mt = <"> media-type <"> + relation-types = relation-type + | <"> relation-type *( 1*SP relation-type ) <"> + relation-type = reg-rel-type | ext-rel-type + reg-rel-type = LOALPHA *( LOALPHA | DIGIT | "." | "-" ) + ext-rel-type = URI + + and from <https://tools.ietf.org/html/rfc5987> + parmname = 1*attr-char + attr-char = ALPHA / DIGIT + / "!" / "#" / "$" / "&" / "+" / "-" / "." + / "^" / "_" / "`" / "|" / "~" + */ + +typedef struct { + apr_pool_t *pool; + const char *relation; + const char *url; +} find_ctx; + +static int find_url(void *baton, const char *key, const char *value) +{ + find_ctx *outer = baton; + + if (!apr_strnatcasecmp("link", key)) { + link_ctx ctx; + + memset(&ctx, 0, sizeof(ctx)); + ctx.s = value; + ctx.slen = strlen(value); + + while (read_link(&ctx)) { + while (skip_param(&ctx)) { + if (ctx.pn_len == 3 && !strncmp("rel", ctx.s + ctx.pn_start, 3) + && pv_contains(&ctx, outer->relation)) { + /* this is the link relation we are looking for */ + outer->url = apr_pstrndup(outer->pool, ctx.s + ctx.link_start, ctx.link_len); + return 0; + } + } + } + } + return 1; +} + +const char *md_link_find_relation(const apr_table_t *headers, + apr_pool_t *pool, const char *relation) +{ + find_ctx ctx; + + memset(&ctx, 0, sizeof(ctx)); + ctx.pool = pool; + ctx.relation = relation; + + apr_table_do(find_url, &ctx, headers, NULL); + + return ctx.url; +} + diff --git a/modules/md/md_util.h b/modules/md/md_util.h new file mode 100644 index 0000000..5b3a2ea --- /dev/null +++ b/modules/md/md_util.h @@ -0,0 +1,148 @@ +/* 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. + */ + +#ifndef mod_md_md_util_h +#define mod_md_md_util_h + +#include <stdio.h> +#include <apr_file_io.h> + +struct apr_array_header_t; +struct apr_table_t; + +/**************************************************************************************************/ +/* pool utils */ + +typedef apr_status_t md_util_action(void *baton, apr_pool_t *p, apr_pool_t *ptemp); +typedef apr_status_t md_util_vaction(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap); + +apr_status_t md_util_pool_do(md_util_action *cb, void *baton, apr_pool_t *p); +apr_status_t md_util_pool_vdo(md_util_vaction *cb, void *baton, apr_pool_t *p, ...); + +/**************************************************************************************************/ +/* string related */ +char *md_util_str_tolower(char *s); + +int md_array_str_index(const struct apr_array_header_t *array, const char *s, + int start, int case_sensitive); + +int md_array_str_eq(const struct apr_array_header_t *a1, + const struct apr_array_header_t *a2, int case_sensitive); + +struct apr_array_header_t *md_array_str_clone(apr_pool_t *p, struct apr_array_header_t *array); + +struct apr_array_header_t *md_array_str_compact(apr_pool_t *p, struct apr_array_header_t *src, + int case_sensitive); + +struct apr_array_header_t *md_array_str_remove(apr_pool_t *p, struct apr_array_header_t *src, + const char *exclude, int case_sensitive); + +int md_array_str_add_missing(struct apr_array_header_t *dest, + struct apr_array_header_t *src, int case_sensitive); + +/**************************************************************************************************/ +/* process execution */ +apr_status_t md_util_exec(apr_pool_t *p, const char *cmd, const char * const *argv, + int *exit_code); + +/**************************************************************************************************/ +/* dns name check */ + +int md_util_is_dns_name(apr_pool_t *p, const char *hostname, int need_fqdn); + +/**************************************************************************************************/ +/* file system related */ + +struct apr_file_t; +struct apr_finfo_t; + +apr_status_t md_util_fopen(FILE **pf, const char *fn, const char *mode); + +apr_status_t md_util_fcreatex(struct apr_file_t **pf, const char *fn, + apr_fileperms_t perms, apr_pool_t *p); + +apr_status_t md_util_path_merge(const char **ppath, apr_pool_t *p, ...); + +apr_status_t md_util_is_dir(const char *path, apr_pool_t *pool); +apr_status_t md_util_is_file(const char *path, apr_pool_t *pool); + +typedef apr_status_t md_util_file_cb(void *baton, struct apr_file_t *f, apr_pool_t *p); + +apr_status_t md_util_freplace(const char *fpath, apr_fileperms_t perms, apr_pool_t *p, + md_util_file_cb *write, void *baton); + +/** + * Remove a file/directory and all files/directories contain up to max_level. If max_level == 0, + * only an empty directory or a file can be removed. + */ +apr_status_t md_util_rm_recursive(const char *fpath, apr_pool_t *p, int max_level); + +typedef apr_status_t md_util_fdo_cb(void *baton, apr_pool_t *p, apr_pool_t *ptemp, + const char *dir, const char *name, + apr_filetype_e ftype); + +apr_status_t md_util_files_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p, + const char *path, ...); + +/** + * Depth first traversal of directory tree starting at path. + */ +apr_status_t md_util_tree_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p, + const char *path, int follow_links); + +apr_status_t md_util_ftree_remove(const char *path, apr_pool_t *p); + +apr_status_t md_text_fread8k(const char **ptext, apr_pool_t *p, const char *fpath); +apr_status_t md_text_fcreatex(const char *fpath, apr_fileperms_t + perms, apr_pool_t *p, const char *text); +apr_status_t md_text_freplace(const char *fpath, apr_fileperms_t perms, + apr_pool_t *p, const char *text); + +/**************************************************************************************************/ +/* base64 url encodings */ +const char *md_util_base64url_encode(const char *data, + apr_size_t len, apr_pool_t *pool); +apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, + apr_pool_t *pool); + +/**************************************************************************************************/ +/* http/url related */ +const char *md_util_schemify(apr_pool_t *p, const char *s, const char *def_scheme); + +apr_status_t md_util_abs_uri_check(apr_pool_t *p, const char *s, const char **perr); +apr_status_t md_util_abs_http_uri_check(apr_pool_t *p, const char *uri, const char **perr); + +const char *md_link_find_relation(const struct apr_table_t *headers, + apr_pool_t *pool, const char *relation); + +/**************************************************************************************************/ +/* retry logic */ + +typedef apr_status_t md_util_try_fn(void *baton, int i); + +apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs, + apr_interval_time_t timeout, apr_interval_time_t start_delay, + apr_interval_time_t max_delay, int backoff); + +/**************************************************************************************************/ +/* date/time related */ + +#define MD_SECS_PER_HOUR (60*60) +#define MD_SECS_PER_DAY (24*MD_SECS_PER_HOUR) + +const char *md_print_duration(apr_pool_t *p, apr_interval_time_t duration); + +#endif /* md_util_h */ diff --git a/modules/md/md_version.h b/modules/md/md_version.h new file mode 100644 index 0000000..48e91a0 --- /dev/null +++ b/modules/md/md_version.h @@ -0,0 +1,42 @@ +/* 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. + */ + +#ifndef mod_md_md_version_h +#define mod_md_md_version_h + +#undef PACKAGE_VERSION +#undef PACKAGE_TARNAME +#undef PACKAGE_STRING +#undef PACKAGE_NAME +#undef PACKAGE_BUGREPORT + +/** + * @macro + * Version number of the md module as c string + */ +#define MOD_MD_VERSION "1.1.17" + +/** + * @macro + * Numerical representation of the version number of the md module + * release. This is a 24 bit number with 8 bits for major number, 8 bits + * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203. + */ +#define MOD_MD_VERSION_NUM 0x010111 + +#define MD_ACME_DEF_URL "https://acme-v01.api.letsencrypt.org/directory" + +#endif /* mod_md_md_version_h */ diff --git a/modules/md/mod_md.c b/modules/md/mod_md.c new file mode 100644 index 0000000..249a0f0 --- /dev/null +++ b/modules/md/mod_md.c @@ -0,0 +1,1460 @@ +/* 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. + */ + +#include <assert.h> +#include <apr_optional.h> +#include <apr_strings.h> + +#include <ap_release.h> +#ifndef AP_ENABLE_EXCEPTION_HOOK +#define AP_ENABLE_EXCEPTION_HOOK 0 +#endif +#include <mpm_common.h> +#include <httpd.h> +#include <http_core.h> +#include <http_protocol.h> +#include <http_request.h> +#include <http_log.h> +#include <http_vhost.h> +#include <ap_listen.h> + +#include "md.h" +#include "md_curl.h" +#include "md_crypt.h" +#include "md_http.h" +#include "md_json.h" +#include "md_store.h" +#include "md_store_fs.h" +#include "md_log.h" +#include "md_reg.h" +#include "md_util.h" +#include "md_version.h" +#include "md_acme.h" +#include "md_acme_authz.h" + +#include "mod_md.h" +#include "mod_md_config.h" +#include "mod_md_os.h" +#include "mod_ssl.h" +#include "mod_watchdog.h" + +static void md_hooks(apr_pool_t *pool); + +AP_DECLARE_MODULE(md) = { + STANDARD20_MODULE_STUFF, + NULL, /* func to create per dir config */ + NULL, /* func to merge per dir config */ + md_config_create_svr, /* func to create per server config */ + md_config_merge_svr, /* func to merge per server config */ + md_cmds, /* command handlers */ + md_hooks, +#if defined(AP_MODULE_FLAG_NONE) + AP_MODULE_FLAG_ALWAYS_MERGE +#endif +}; + +static void md_merge_srv(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p) +{ + if (!md->sc) { + md->sc = base_sc; + } + + if (!md->ca_url) { + md->ca_url = md_config_gets(md->sc, MD_CONFIG_CA_URL); + } + if (!md->ca_proto) { + md->ca_proto = md_config_gets(md->sc, MD_CONFIG_CA_PROTO); + } + if (!md->ca_agreement) { + md->ca_agreement = md_config_gets(md->sc, MD_CONFIG_CA_AGREEMENT); + } + if (md->sc->s->server_admin && strcmp(DEFAULT_ADMIN, md->sc->s->server_admin)) { + apr_array_clear(md->contacts); + APR_ARRAY_PUSH(md->contacts, const char *) = + md_util_schemify(p, md->sc->s->server_admin, "mailto"); + } + if (md->drive_mode == MD_DRIVE_DEFAULT) { + md->drive_mode = md_config_geti(md->sc, MD_CONFIG_DRIVE_MODE); + } + if (md->renew_norm <= 0 && md->renew_window <= 0) { + md->renew_norm = md_config_get_interval(md->sc, MD_CONFIG_RENEW_NORM); + md->renew_window = md_config_get_interval(md->sc, MD_CONFIG_RENEW_WINDOW); + } + if (md->transitive < 0) { + md->transitive = md_config_geti(md->sc, MD_CONFIG_TRANSITIVE); + } + if (!md->ca_challenges && md->sc->ca_challenges) { + md->ca_challenges = apr_array_copy(p, md->sc->ca_challenges); + } + if (!md->pkey_spec) { + md->pkey_spec = md->sc->pkey_spec; + + } + if (md->require_https < 0) { + md->require_https = md_config_geti(md->sc, MD_CONFIG_REQUIRE_HTTPS); + } + if (md->must_staple < 0) { + md->must_staple = md_config_geti(md->sc, MD_CONFIG_MUST_STAPLE); + } +} + +static apr_status_t check_coverage(md_t *md, const char *domain, server_rec *s, apr_pool_t *p) +{ + if (md_contains(md, domain, 0)) { + return APR_SUCCESS; + } + else if (md->transitive) { + APR_ARRAY_PUSH(md->domains, const char*) = apr_pstrdup(p, domain); + return APR_SUCCESS; + } + else { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(10040) + "Virtual Host %s:%d matches Managed Domain '%s', but the " + "name/alias %s itself is not managed. A requested MD certificate " + "will not match ServerName.", + s->server_hostname, s->port, md->name, domain); + return APR_EINVAL; + } +} + +static apr_status_t md_covers_server(md_t *md, server_rec *s, apr_pool_t *p) +{ + apr_status_t rv; + const char *name; + int i; + + if (APR_SUCCESS == (rv = check_coverage(md, s->server_hostname, s, p)) && s->names) { + for (i = 0; i < s->names->nelts; ++i) { + name = APR_ARRAY_IDX(s->names, i, const char*); + if (APR_SUCCESS != (rv = check_coverage(md, name, s, p))) { + break; + } + } + } + return rv; +} + +static int matches_port_somewhere(server_rec *s, int port) +{ + server_addr_rec *sa; + + for (sa = s->addrs; sa; sa = sa->next) { + if (sa->host_port == port) { + /* host_addr might be general (0.0.0.0) or specific, we count this as match */ + return 1; + } + if (sa->host_port == 0) { + /* wildcard port, answers to all ports. Rare, but may work. */ + return 1; + } + } + return 0; +} + +static int uses_port_only(server_rec *s, int port) +{ + server_addr_rec *sa; + int match = 0; + for (sa = s->addrs; sa; sa = sa->next) { + if (sa->host_port == port) { + /* host_addr might be general (0.0.0.0) or specific, we count this as match */ + match = 1; + } + else { + /* uses other port/wildcard */ + return 0; + } + } + return match; +} + +static apr_status_t assign_to_servers(md_t *md, server_rec *base_server, + apr_pool_t *p, apr_pool_t *ptemp) +{ + server_rec *s, *s_https; + request_rec r; + md_srv_conf_t *sc; + md_mod_conf_t *mc; + apr_status_t rv = APR_SUCCESS; + int i; + const char *domain; + apr_array_header_t *servers; + + sc = md_config_get(base_server); + mc = sc->mc; + + /* Assign the MD to all server_rec configs that it matches. If there already + * is an assigned MD not equal this one, the configuration is in error. + */ + memset(&r, 0, sizeof(r)); + servers = apr_array_make(ptemp, 5, sizeof(server_rec*)); + + for (s = base_server; s; s = s->next) { + if (!mc->manage_base_server && s == base_server) { + /* we shall not assign ourselves to the base server */ + continue; + } + + r.server = s; + for (i = 0; i < md->domains->nelts; ++i) { + domain = APR_ARRAY_IDX(md->domains, i, const char*); + + if (ap_matches_request_vhost(&r, domain, s->port)) { + /* Create a unique md_srv_conf_t record for this server, if there is none yet */ + sc = md_config_get_unique(s, p); + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10041) + "Server %s:%d matches md %s (config %s)", + s->server_hostname, s->port, md->name, sc->name); + + if (sc->assigned == md) { + /* already matched via another domain name */ + goto next_server; + } + else if (sc->assigned) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10042) + "conflict: MD %s matches server %s, but MD %s also matches.", + md->name, s->server_hostname, sc->assigned->name); + return APR_EINVAL; + } + + /* If this server_rec is only for http: requests. Defined + * alias names to not matter for this MD. + * (see gh issue https://github.com/icing/mod_md/issues/57) + * Otherwise, if server has name or an alias not covered, + * it is by default auto-added (config transitive). + * If mode is "manual", a generated certificate will not match + * all necessary names. */ + if ((!mc->local_80 || !uses_port_only(s, mc->local_80)) + && APR_SUCCESS != (rv = md_covers_server(md, s, p))) { + return rv; + } + + sc->assigned = md; + APR_ARRAY_PUSH(servers, server_rec*) = s; + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10043) + "Managed Domain %s applies to vhost %s:%d", md->name, + s->server_hostname, s->port); + + goto next_server; + } + } + next_server: + continue; + } + + if (APR_SUCCESS == rv) { + if (apr_is_empty_array(servers)) { + if (md->drive_mode != MD_DRIVE_ALWAYS) { + /* Not an error, but looks suspicious */ + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, base_server, APLOGNO(10045) + "No VirtualHost matches Managed Domain %s", md->name); + APR_ARRAY_PUSH(mc->unused_names, const char*) = md->name; + } + } + else { + const char *uri; + + /* Found matching server_rec's. Collect all 'ServerAdmin's into MD's contact list */ + apr_array_clear(md->contacts); + for (i = 0; i < servers->nelts; ++i) { + s = APR_ARRAY_IDX(servers, i, server_rec*); + if (s->server_admin && strcmp(DEFAULT_ADMIN, s->server_admin)) { + uri = md_util_schemify(p, s->server_admin, "mailto"); + if (md_array_str_index(md->contacts, uri, 0, 0) < 0) { + APR_ARRAY_PUSH(md->contacts, const char *) = uri; + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10044) + "%s: added contact %s", md->name, uri); + } + } + } + + if (md->require_https > MD_REQUIRE_OFF) { + /* We require https for this MD, but do we have port 443 (or a mapped one) + * available? */ + if (mc->local_443 <= 0) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10105) + "MDPortMap says there is no port for https (443), " + "but MD %s is configured to require https. This " + "only works when a 443 port is available.", md->name); + return APR_EINVAL; + + } + + /* Ok, we know which local port represents 443, do we have a server_rec + * for MD that has addresses with port 443? */ + s_https = NULL; + for (i = 0; i < servers->nelts; ++i) { + s = APR_ARRAY_IDX(servers, i, server_rec*); + if (matches_port_somewhere(s, mc->local_443)) { + s_https = s; + break; + } + } + + if (!s_https) { + /* Did not find any server_rec that matches this MD *and* has an + * s->addrs match for the https port. Suspicious. */ + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, base_server, APLOGNO(10106) + "MD %s is configured to require https, but there seems to be " + "no VirtualHost for it that has port %d in its address list. " + "This looks as if it will not work.", + md->name, mc->local_443); + } + } + } + + } + return rv; +} + +static apr_status_t md_calc_md_list(apr_pool_t *p, apr_pool_t *plog, + apr_pool_t *ptemp, server_rec *base_server) +{ + md_srv_conf_t *sc; + md_mod_conf_t *mc; + md_t *md, *omd; + const char *domain; + apr_status_t rv = APR_SUCCESS; + ap_listen_rec *lr; + apr_sockaddr_t *sa; + int i, j; + + (void)plog; + sc = md_config_get(base_server); + mc = sc->mc; + + mc->can_http = 0; + mc->can_https = 0; + + for (lr = ap_listeners; lr; lr = lr->next) { + for (sa = lr->bind_addr; sa; sa = sa->next) { + if (sa->port == mc->local_80 + && (!lr->protocol || !strncmp("http", lr->protocol, 4))) { + mc->can_http = 1; + } + else if (sa->port == mc->local_443 + && (!lr->protocol || !strncmp("http", lr->protocol, 4))) { + mc->can_https = 1; + } + } + } + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10037) + "server seems%s reachable via http: (port 80->%d) " + "and%s reachable via https: (port 443->%d) ", + mc->can_http? "" : " not", mc->local_80, + mc->can_https? "" : " not", mc->local_443); + + /* Complete the properties of the MDs, now that we have the complete, merged + * server configurations. + */ + for (i = 0; i < mc->mds->nelts; ++i) { + md = APR_ARRAY_IDX(mc->mds, i, md_t*); + md_merge_srv(md, sc, p); + + /* Check that we have no overlap with the MDs already completed */ + for (j = 0; j < i; ++j) { + omd = APR_ARRAY_IDX(mc->mds, j, md_t*); + if ((domain = md_common_name(md, omd)) != NULL) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10038) + "two Managed Domains have an overlap in domain '%s'" + ", first definition in %s(line %d), second in %s(line %d)", + domain, md->defn_name, md->defn_line_number, + omd->defn_name, omd->defn_line_number); + return APR_EINVAL; + } + } + + /* Assign MD to the server_rec configs that it matches. Perform some + * last finishing touches on the MD. */ + if (APR_SUCCESS != (rv = assign_to_servers(md, base_server, p, ptemp))) { + return rv; + } + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10039) + "Completed MD[%s, CA=%s, Proto=%s, Agreement=%s, Drive=%d, renew=%ld]", + md->name, md->ca_url, md->ca_proto, md->ca_agreement, + md->drive_mode, (long)md->renew_window); + } + + return rv; +} + +/**************************************************************************************************/ +/* store & registry setup */ + +static apr_status_t store_file_ev(void *baton, struct md_store_t *store, + md_store_fs_ev_t ev, int group, + const char *fname, apr_filetype_e ftype, + apr_pool_t *p) +{ + server_rec *s = baton; + apr_status_t rv; + + (void)store; + ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, s, "store event=%d on %s %s (group %d)", + ev, (ftype == APR_DIR)? "dir" : "file", fname, group); + + /* Directories in group CHALLENGES and STAGING are written to by our watchdog, + * running on certain mpms in a child process under a different user. Give them + * ownership. + */ + if (ftype == APR_DIR) { + switch (group) { + case MD_SG_CHALLENGES: + case MD_SG_STAGING: + rv = md_make_worker_accessible(fname, p); + if (APR_ENOTIMPL != rv) { + return rv; + } + break; + default: + break; + } + } + return APR_SUCCESS; +} + +static apr_status_t check_group_dir(md_store_t *store, md_store_group_t group, + apr_pool_t *p, server_rec *s) +{ + const char *dir; + apr_status_t rv; + + if (APR_SUCCESS == (rv = md_store_get_fname(&dir, store, group, NULL, NULL, p)) + && APR_SUCCESS == (rv = apr_dir_make_recursive(dir, MD_FPROT_D_UALL_GREAD, p))) { + rv = store_file_ev(s, store, MD_S_FS_EV_CREATED, group, dir, APR_DIR, p); + } + return rv; +} + +static apr_status_t setup_store(md_store_t **pstore, md_mod_conf_t *mc, + apr_pool_t *p, server_rec *s) +{ + const char *base_dir; + apr_status_t rv; + MD_CHK_VARS; + + base_dir = ap_server_root_relative(p, mc->base_dir); + + if (!MD_OK(md_store_fs_init(pstore, p, base_dir))) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10046)"setup store for %s", base_dir); + goto out; + } + + md_store_fs_set_event_cb(*pstore, store_file_ev, s); + if ( !MD_OK(check_group_dir(*pstore, MD_SG_CHALLENGES, p, s)) + || !MD_OK(check_group_dir(*pstore, MD_SG_STAGING, p, s)) + || !MD_OK(check_group_dir(*pstore, MD_SG_ACCOUNTS, p, s))) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10047) + "setup challenges directory, call %s", MD_LAST_CHK); + } + +out: + return rv; +} + +static apr_status_t setup_reg(md_reg_t **preg, apr_pool_t *p, server_rec *s, + int can_http, int can_https) +{ + md_srv_conf_t *sc; + md_mod_conf_t *mc; + md_store_t *store; + apr_status_t rv; + MD_CHK_VARS; + + sc = md_config_get(s); + mc = sc->mc; + + if ( MD_OK(setup_store(&store, mc, p, s)) + && MD_OK(md_reg_init(preg, p, store, mc->proxy_url))) { + mc->reg = *preg; + return md_reg_set_props(*preg, p, can_http, can_https); + } + return rv; +} + +/**************************************************************************************************/ +/* logging setup */ + +static server_rec *log_server; + +static int log_is_level(void *baton, apr_pool_t *p, md_log_level_t level) +{ + (void)baton; + (void)p; + if (log_server) { + return APLOG_IS_LEVEL(log_server, (int)level); + } + return level <= MD_LOG_INFO; +} + +#define LOG_BUF_LEN 16*1024 + +static void log_print(const char *file, int line, md_log_level_t level, + apr_status_t rv, void *baton, apr_pool_t *p, const char *fmt, va_list ap) +{ + if (log_is_level(baton, p, level)) { + char buffer[LOG_BUF_LEN]; + + memset(buffer, 0, sizeof(buffer)); + apr_vsnprintf(buffer, LOG_BUF_LEN-1, fmt, ap); + buffer[LOG_BUF_LEN-1] = '\0'; + + if (log_server) { + ap_log_error(file, line, APLOG_MODULE_INDEX, level, rv, log_server, "%s",buffer); + } + else { + ap_log_perror(file, line, APLOG_MODULE_INDEX, level, rv, p, "%s", buffer); + } + } +} + +/**************************************************************************************************/ +/* lifecycle */ + +static apr_status_t cleanup_setups(void *dummy) +{ + (void)dummy; + log_server = NULL; + return APR_SUCCESS; +} + +static void init_setups(apr_pool_t *p, server_rec *base_server) +{ + log_server = base_server; + apr_pool_cleanup_register(p, NULL, cleanup_setups, apr_pool_cleanup_null); +} + +/**************************************************************************************************/ +/* mod_ssl interface */ + +static APR_OPTIONAL_FN_TYPE(ssl_is_https) *opt_ssl_is_https; + +static void init_ssl(void) +{ + opt_ssl_is_https = APR_RETRIEVE_OPTIONAL_FN(ssl_is_https); +} + +/**************************************************************************************************/ +/* watchdog based impl. */ + +#define MD_WATCHDOG_NAME "_md_" + +static APR_OPTIONAL_FN_TYPE(ap_watchdog_get_instance) *wd_get_instance; +static APR_OPTIONAL_FN_TYPE(ap_watchdog_register_callback) *wd_register_callback; +static APR_OPTIONAL_FN_TYPE(ap_watchdog_set_callback_interval) *wd_set_interval; + +typedef struct { + md_t *md; + + int stalled; + int renewed; + int renewal_notified; + apr_time_t restart_at; + int need_restart; + int restart_processed; + + apr_status_t last_rv; + apr_time_t next_check; + int error_runs; +} md_job_t; + +typedef struct { + apr_pool_t *p; + server_rec *s; + md_mod_conf_t *mc; + ap_watchdog_t *watchdog; + + apr_time_t next_change; + + apr_array_header_t *jobs; + md_reg_t *reg; +} md_watchdog; + +static void assess_renewal(md_watchdog *wd, md_job_t *job, apr_pool_t *ptemp) +{ + apr_time_t now = apr_time_now(); + if (now >= job->restart_at) { + job->need_restart = 1; + ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, wd->s, + "md(%s): has been renewed, needs restart now", job->md->name); + } + else { + job->next_check = job->restart_at; + + if (job->renewal_notified) { + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, wd->s, + "%s: renewed cert valid in %s", + job->md->name, md_print_duration(ptemp, job->restart_at - now)); + } + else { + char ts[APR_RFC822_DATE_LEN]; + + apr_rfc822_date(ts, job->restart_at); + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, wd->s, APLOGNO(10051) + "%s: has been renewed successfully and should be activated at %s" + " (this requires a server restart latest in %s)", + job->md->name, ts, md_print_duration(ptemp, job->restart_at - now)); + job->renewal_notified = 1; + } + } +} + +static apr_status_t load_job_props(md_reg_t *reg, md_job_t *job, apr_pool_t *p) +{ + md_store_t *store = md_reg_store_get(reg); + md_json_t *jprops; + apr_status_t rv; + + rv = md_store_load_json(store, MD_SG_STAGING, job->md->name, + MD_FN_JOB, &jprops, p); + if (APR_SUCCESS == rv) { + job->restart_processed = md_json_getb(jprops, MD_KEY_PROCESSED, NULL); + job->error_runs = (int)md_json_getl(jprops, MD_KEY_ERRORS, NULL); + } + return rv; +} + +static apr_status_t save_job_props(md_reg_t *reg, md_job_t *job, apr_pool_t *p) +{ + md_store_t *store = md_reg_store_get(reg); + md_json_t *jprops; + apr_status_t rv; + + rv = md_store_load_json(store, MD_SG_STAGING, job->md->name, MD_FN_JOB, &jprops, p); + if (APR_STATUS_IS_ENOENT(rv)) { + jprops = md_json_create(p); + rv = APR_SUCCESS; + } + if (APR_SUCCESS == rv) { + md_json_setb(job->restart_processed, jprops, MD_KEY_PROCESSED, NULL); + md_json_setl(job->error_runs, jprops, MD_KEY_ERRORS, NULL); + rv = md_store_save_json(store, p, MD_SG_STAGING, job->md->name, + MD_FN_JOB, jprops, 0); + } + return rv; +} + +static apr_status_t check_job(md_watchdog *wd, md_job_t *job, apr_pool_t *ptemp) +{ + apr_status_t rv = APR_SUCCESS; + apr_time_t valid_from, delay; + int errored, renew, error_runs; + char ts[APR_RFC822_DATE_LEN]; + + if (apr_time_now() < job->next_check) { + /* Job needs to wait */ + return APR_EAGAIN; + } + + job->next_check = 0; + error_runs = job->error_runs; + + if (job->md->state == MD_S_MISSING) { + job->stalled = 1; + } + + if (job->stalled) { + /* Missing information, this will not change until configuration + * is changed and server restarted */ + rv = APR_INCOMPLETE; + ++job->error_runs; + goto out; + } + else if (job->renewed) { + assess_renewal(wd, job, ptemp); + } + else if (APR_SUCCESS == (rv = md_reg_assess(wd->reg, job->md, &errored, &renew, wd->p))) { + if (errored) { + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10050) + "md(%s): in error state", job->md->name); + } + else if (renew) { + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10052) + "md(%s): state=%d, driving", job->md->name, job->md->state); + + rv = md_reg_stage(wd->reg, job->md, NULL, 0, &valid_from, ptemp); + + if (APR_SUCCESS == rv) { + job->renewed = 1; + job->restart_at = valid_from; + assess_renewal(wd, job, ptemp); + } + } + else { + /* Renew is not necessary yet, leave job->next_check as 0 since + * that keeps the default schedule of running twice a day. */ + apr_rfc822_date(ts, job->md->expires); + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10053) + "md(%s): no need to renew yet, cert expires %s", job->md->name, ts); + } + } + + if (APR_SUCCESS == rv) { + job->error_runs = 0; + } + else { + ap_log_error( APLOG_MARK, APLOG_ERR, rv, wd->s, APLOGNO(10056) + "processing %s", job->md->name); + ++job->error_runs; + /* back off duration, depending on the errors we encounter in a row */ + delay = apr_time_from_sec(5 << (job->error_runs - 1)); + if (delay > apr_time_from_sec(60*60)) { + delay = apr_time_from_sec(60*60); + } + job->next_check = apr_time_now() + delay; + ap_log_error(APLOG_MARK, APLOG_INFO, 0, wd->s, APLOGNO(10057) + "%s: encountered error for the %d. time, next run in %s", + job->md->name, job->error_runs, md_print_duration(ptemp, delay)); + } + +out: + if (error_runs != job->error_runs) { + apr_status_t rv2 = save_job_props(wd->reg, job, ptemp); + ap_log_error(APLOG_MARK, APLOG_TRACE1, rv2, wd->s, "%s: saving job props", job->md->name); + } + + job->last_rv = rv; + return rv; +} + +static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp) +{ + md_watchdog *wd = baton; + apr_status_t rv = APR_SUCCESS; + md_job_t *job; + apr_time_t next_run, now; + int restart = 0; + int i; + + switch (state) { + case AP_WATCHDOG_STATE_STARTING: + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10054) + "md watchdog start, auto drive %d mds", wd->jobs->nelts); + assert(wd->reg); + + for (i = 0; i < wd->jobs->nelts; ++i) { + job = APR_ARRAY_IDX(wd->jobs, i, md_job_t *); + load_job_props(wd->reg, job, ptemp); + } + break; + case AP_WATCHDOG_STATE_RUNNING: + + wd->next_change = 0; + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10055) + "md watchdog run, auto drive %d mds", wd->jobs->nelts); + + /* normally, we'd like to run at least twice a day */ + next_run = apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY / 2); + + /* Check on all the jobs we have */ + for (i = 0; i < wd->jobs->nelts; ++i) { + job = APR_ARRAY_IDX(wd->jobs, i, md_job_t *); + + rv = check_job(wd, job, ptemp); + + if (job->need_restart && !job->restart_processed) { + restart = 1; + } + if (job->next_check && job->next_check < next_run) { + next_run = job->next_check; + } + } + + now = apr_time_now(); + if (APLOGdebug(wd->s)) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10107) + "next run in %s", md_print_duration(ptemp, next_run - now)); + } + wd_set_interval(wd->watchdog, next_run - now, wd, run_watchdog); + break; + + case AP_WATCHDOG_STATE_STOPPING: + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10058) + "md watchdog stopping"); + break; + } + + if (restart) { + const char *action, *names = ""; + int n; + + for (i = 0, n = 0; i < wd->jobs->nelts; ++i) { + job = APR_ARRAY_IDX(wd->jobs, i, md_job_t *); + if (job->need_restart && !job->restart_processed) { + names = apr_psprintf(ptemp, "%s%s%s", names, n? " " : "", job->md->name); + ++n; + } + } + + if (n > 0) { + int notified = 1; + + /* Run notify command for ready MDs (if configured) and persist that + * we have done so. This process might be reaped after n requests or die + * of another cause. The one taking over the watchdog need to notify again. + */ + if (wd->mc->notify_cmd) { + const char * const *argv; + const char *cmdline; + int exit_code; + + cmdline = apr_psprintf(ptemp, "%s %s", wd->mc->notify_cmd, names); + apr_tokenize_to_argv(cmdline, (char***)&argv, ptemp); + if (APR_SUCCESS == (rv = md_util_exec(ptemp, argv[0], argv, &exit_code))) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, wd->s, APLOGNO(10108) + "notify command '%s' returned %d", + wd->mc->notify_cmd, exit_code); + } + else { + if (APR_EINCOMPLETE == rv && exit_code) { + rv = 0; + } + ap_log_error(APLOG_MARK, APLOG_ERR, rv, wd->s, APLOGNO(10109) + "executing MDNotifyCmd %s returned %d", + wd->mc->notify_cmd, exit_code); + notified = 0; + } + } + + if (notified) { + /* persist the jobs that were notified */ + for (i = 0, n = 0; i < wd->jobs->nelts; ++i) { + job = APR_ARRAY_IDX(wd->jobs, i, md_job_t *); + if (job->need_restart && !job->restart_processed) { + job->restart_processed = 1; + save_job_props(wd->reg, job, ptemp); + } + } + } + + /* FIXME: the server needs to start gracefully to take the new certificate in. + * This poses a variety of problems to solve satisfactory for everyone: + * - I myself, have no implementation for Windows + * - on *NIX, child processes run with less privileges, preventing + * the signal based restart trigger to work + * - admins want better control of timing windows for restarts, e.g. + * during less busy hours/days. + */ + rv = md_server_graceful(ptemp, wd->s); + if (APR_ENOTIMPL == rv) { + /* self-graceful restart not supported in this setup */ + action = " and changes will be activated on next (graceful) server restart."; + } + else { + action = " and server has been asked to restart now."; + } + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, wd->s, APLOGNO(10059) + "The Managed Domain%s %s %s been setup%s", + (n > 1)? "s" : "", names, (n > 1)? "have" : "has", action); + } + } + + return APR_SUCCESS; +} + +static apr_status_t start_watchdog(apr_array_header_t *names, apr_pool_t *p, + md_reg_t *reg, server_rec *s, md_mod_conf_t *mc) +{ + apr_allocator_t *allocator; + md_watchdog *wd; + apr_pool_t *wdp; + apr_status_t rv; + const char *name; + md_t *md; + md_job_t *job; + int i, errored, renew; + + wd_get_instance = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_get_instance); + wd_register_callback = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_register_callback); + wd_set_interval = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_set_callback_interval); + + if (!wd_get_instance || !wd_register_callback || !wd_set_interval) { + ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s, APLOGNO(10061) "mod_watchdog is required"); + return !OK; + } + + /* We want our own pool with own allocator to keep data across watchdog invocations */ + apr_allocator_create(&allocator); + apr_allocator_max_free_set(allocator, ap_max_mem_free); + rv = apr_pool_create_ex(&wdp, p, NULL, allocator); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10062) "md_watchdog: create pool"); + return rv; + } + apr_allocator_owner_set(allocator, wdp); + apr_pool_tag(wdp, "md_watchdog"); + + wd = apr_pcalloc(wdp, sizeof(*wd)); + wd->p = wdp; + wd->reg = reg; + wd->s = s; + wd->mc = mc; + + wd->jobs = apr_array_make(wd->p, 10, sizeof(md_job_t *)); + for (i = 0; i < names->nelts; ++i) { + name = APR_ARRAY_IDX(names, i, const char *); + md = md_reg_get(wd->reg, name, wd->p); + if (md) { + md_reg_assess(wd->reg, md, &errored, &renew, wd->p); + if (errored) { + ap_log_error( APLOG_MARK, APLOG_WARNING, 0, wd->s, APLOGNO(10063) + "md(%s): seems errored. Will not process this any further.", name); + } + else { + job = apr_pcalloc(wd->p, sizeof(*job)); + + job->md = md; + APR_ARRAY_PUSH(wd->jobs, md_job_t*) = job; + + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10064) + "md(%s): state=%d, driving", name, md->state); + + load_job_props(reg, job, wd->p); + if (job->error_runs) { + /* We are just restarting. If we encounter jobs that had errors + * running the protocol on previous staging runs, we reset + * the staging area for it, in case we persisted something that + * causes a loop. */ + md_store_t *store = md_reg_store_get(wd->reg); + + md_store_purge(store, p, MD_SG_STAGING, job->md->name); + md_store_purge(store, p, MD_SG_CHALLENGES, job->md->name); + } + } + } + } + + if (!wd->jobs->nelts) { + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10065) + "no managed domain in state to drive, no watchdog needed, " + "will check again on next server (graceful) restart"); + apr_pool_destroy(wd->p); + return APR_SUCCESS; + } + + if (APR_SUCCESS != (rv = wd_get_instance(&wd->watchdog, MD_WATCHDOG_NAME, 0, 1, wd->p))) { + ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, APLOGNO(10066) + "create md watchdog(%s)", MD_WATCHDOG_NAME); + return rv; + } + rv = wd_register_callback(wd->watchdog, 0, wd, run_watchdog); + ap_log_error(APLOG_MARK, rv? APLOG_CRIT : APLOG_DEBUG, rv, s, APLOGNO(10067) + "register md watchdog(%s)", MD_WATCHDOG_NAME); + return rv; +} + +static void load_stage_sets(apr_array_header_t *names, apr_pool_t *p, + md_reg_t *reg, server_rec *s) +{ + const char *name; + apr_status_t rv; + int i; + + for (i = 0; i < names->nelts; ++i) { + name = APR_ARRAY_IDX(names, i, const char*); + if (APR_SUCCESS == (rv = md_reg_load(reg, name, p))) { + ap_log_error( APLOG_MARK, APLOG_INFO, rv, s, APLOGNO(10068) + "%s: staged set activated", name); + } + else if (!APR_STATUS_IS_ENOENT(rv)) { + ap_log_error( APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10069) + "%s: error loading staged set", name); + } + } + return; +} + +static apr_status_t md_post_config(apr_pool_t *p, apr_pool_t *plog, + apr_pool_t *ptemp, server_rec *s) +{ + void *data = NULL; + const char *mod_md_init_key = "mod_md_init_counter"; + md_srv_conf_t *sc; + md_mod_conf_t *mc; + md_reg_t *reg; + const md_t *md; + apr_array_header_t *drive_names; + apr_status_t rv = APR_SUCCESS; + int i, dry_run = 0; + + apr_pool_userdata_get(&data, mod_md_init_key, s->process->pool); + if (data == NULL) { + /* At the first start, httpd makes a config check dry run. It + * runs all config hooks to check if it can. If so, it does + * this all again and starts serving requests. + * + * This is known. + * + * On a dry run, we therefore do all the cheap config things we + * need to do. Because otherwise mod_ssl fails because it calls + * us unprepared. + * But synching our configuration with the md store + * and determining which domains to drive and start a watchdog + * and all that, we do not. + */ + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10070) + "initializing post config dry run"); + apr_pool_userdata_set((const void *)1, mod_md_init_key, + apr_pool_cleanup_null, s->process->pool); + dry_run = 1; + } + else { + ap_log_error( APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(10071) + "mod_md (v%s), initializing...", MOD_MD_VERSION); + } + + (void)plog; + init_setups(p, s); + md_log_set(log_is_level, log_print, NULL); + + /* Check uniqueness of MDs, calculate global, configured MD list. + * If successful, we have a list of MD definitions that do not overlap. */ + /* We also need to find out if we can be reached on 80/443 from the outside (e.g. the CA) */ + if (APR_SUCCESS != (rv = md_calc_md_list(p, plog, ptemp, s))) { + return rv; + } + + md_config_post_config(s, p); + sc = md_config_get(s); + mc = sc->mc; + + /* Synchronize the definitions we now have with the store via a registry (reg). */ + if (APR_SUCCESS != (rv = setup_reg(®, p, s, mc->can_http, mc->can_https))) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10072) + "setup md registry"); + goto out; + } + + if (APR_SUCCESS != (rv = md_reg_sync(reg, p, ptemp, mc->mds))) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10073) + "synching %d mds to registry", mc->mds->nelts); + } + + /* Determine the managed domains that are in auto drive_mode. For those, + * determine in which state they are: + * - UNKNOWN: should not happen, report, don't drive + * - ERROR: something we do not know how to fix, report, don't drive + * - INCOMPLETE/EXPIRED: need to drive them right away + * - COMPLETE: determine when cert expires, drive when the time comes + * + * Start the watchdog if we have anything, now or in the future. + */ + drive_names = apr_array_make(ptemp, mc->mds->nelts+1, sizeof(const char *)); + for (i = 0; i < mc->mds->nelts; ++i) { + md = APR_ARRAY_IDX(mc->mds, i, const md_t *); + switch (md->drive_mode) { + case MD_DRIVE_AUTO: + if (md_array_str_index(mc->unused_names, md->name, 0, 0) >= 0) { + break; + } + /* fall through */ + case MD_DRIVE_ALWAYS: + APR_ARRAY_PUSH(drive_names, const char *) = md->name; + break; + default: + /* leave out */ + break; + } + } + + init_ssl(); + + if (dry_run) { + goto out; + } + + /* If there are MDs to drive, start a watchdog to check on them regularly */ + if (drive_names->nelts > 0) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, APLOGNO(10074) + "%d out of %d mds are configured for auto-drive", + drive_names->nelts, mc->mds->nelts); + + load_stage_sets(drive_names, p, reg, s); + md_http_use_implementation(md_curl_get_impl(p)); + rv = start_watchdog(drive_names, p, reg, s, mc); + } + else { + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10075) + "no mds to auto drive, no watchdog needed"); + } +out: + return rv; +} + +/**************************************************************************************************/ +/* Access API to other httpd components */ + +static int md_is_managed(server_rec *s) +{ + md_srv_conf_t *conf = md_config_get(s); + + if (conf && conf->assigned) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10076) + "%s: manages server %s", conf->assigned->name, s->server_hostname); + return 1; + } + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, + "server %s is not managed", s->server_hostname); + return 0; +} + +static apr_status_t setup_fallback_cert(md_store_t *store, const md_t *md, + server_rec *s, apr_pool_t *p) +{ + md_pkey_t *pkey; + md_cert_t *cert; + md_pkey_spec_t spec; + apr_status_t rv; + MD_CHK_VARS; + + spec.type = MD_PKEY_TYPE_RSA; + spec.params.rsa.bits = MD_PKEY_RSA_BITS_DEF; + + if ( !MD_OK(md_pkey_gen(&pkey, p, &spec)) + || !MD_OK(md_store_save(store, p, MD_SG_DOMAINS, md->name, + MD_FN_FALLBACK_PKEY, MD_SV_PKEY, (void*)pkey, 0)) + || !MD_OK(md_cert_self_sign(&cert, "Apache Managed Domain Fallback", + md->domains, pkey, apr_time_from_sec(14 * MD_SECS_PER_DAY), p)) + || !MD_OK(md_store_save(store, p, MD_SG_DOMAINS, md->name, + MD_FN_FALLBACK_CERT, MD_SV_CERT, (void*)cert, 0))) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, + "%s: setup fallback certificate, call %s", md->name, MD_LAST_CHK); + } + return rv; +} + +static int fexists(const char *fname, apr_pool_t *p) +{ + return (*fname && APR_SUCCESS == md_util_is_file(fname, p)); +} + +static apr_status_t md_get_certificate(server_rec *s, apr_pool_t *p, + const char **pkeyfile, const char **pcertfile) +{ + apr_status_t rv = APR_ENOENT; + md_srv_conf_t *sc; + md_reg_t *reg; + md_store_t *store; + const md_t *md; + MD_CHK_VARS; + + *pkeyfile = NULL; + *pcertfile = NULL; + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10113) + "md_get_certificate called for vhost %s.", s->server_hostname); + + sc = md_config_get(s); + if (!sc) { + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, + "asked for certificate of server %s which has no md config", + s->server_hostname); + return APR_ENOENT; + } + + if (!sc->assigned) { + /* Hmm, mod_ssl (or someone like it) asks for certificates for a server + * where we did not assign a MD to. Either the user forgot to configure + * that server with SSL certs, has misspelled a server name or we have + * a bug that prevented us from taking responsibility for this server. + * Either way, make some polite noise */ + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, s, APLOGNO(10114) + "asked for certificate of server %s which has no MD assigned. This " + "could be ok, but most likely it is either a misconfiguration or " + "a bug. Please check server names and MD names carefully and if " + "everything checks open, please open an issue.", + s->server_hostname); + return APR_ENOENT; + } + + assert(sc->mc); + reg = sc->mc->reg; + assert(reg); + + md = md_reg_get(reg, sc->assigned->name, p); + if (!md) { + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, APLOGNO(10115) + "unable to hand out certificates, as registry can no longer " + "find MD '%s'.", sc->assigned->name); + return APR_ENOENT; + } + + if (!MD_OK(md_reg_get_cred_files(reg, md, p, pkeyfile, pcertfile))) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10110) + "retrieving credentials for MD %s", md->name); + return rv; + } + + if (!fexists(*pkeyfile, p) || !fexists(*pcertfile, p)) { + /* Provide temporary, self-signed certificate as fallback, so that + * clients do not get obscure TLS handshake errors or will see a fallback + * virtual host that is not intended to be served here. */ + store = md_reg_store_get(reg); + assert(store); + + md_store_get_fname(pkeyfile, store, MD_SG_DOMAINS, + md->name, MD_FN_FALLBACK_PKEY, p); + md_store_get_fname(pcertfile, store, MD_SG_DOMAINS, + md->name, MD_FN_FALLBACK_CERT, p); + if (!fexists(*pkeyfile, p) || !fexists(*pcertfile, p)) { + if (!MD_OK(setup_fallback_cert(store, md, s, p))) { + return rv; + } + } + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10116) + "%s: providing fallback certificate for server %s", + md->name, s->server_hostname); + return APR_EAGAIN; + } + + /* We have key and cert files, but they might no longer be valid or not + * match all domain names. Still use these files for now, but indicate that + * resources should no longer be served until we have a new certificate again. */ + if (md->state != MD_S_COMPLETE) { + rv = APR_EAGAIN; + } + ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, APLOGNO(10077) + "%s: providing certificate for server %s", md->name, s->server_hostname); + return rv; +} + +static int compat_warned; +static apr_status_t md_get_credentials(server_rec *s, apr_pool_t *p, + const char **pkeyfile, + const char **pcertfile, + const char **pchainfile) +{ + *pchainfile = NULL; + if (!compat_warned) { + compat_warned = 1; + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, /* no APLOGNO */ + "You are using mod_md with an old patch to mod_ssl. This will " + " work for now, but support will be dropped in a future release."); + } + return md_get_certificate(s, p, pkeyfile, pcertfile); +} + +static int md_is_challenge(conn_rec *c, const char *servername, + X509 **pcert, EVP_PKEY **pkey) +{ + md_srv_conf_t *sc; + apr_size_t slen, sufflen = sizeof(MD_TLSSNI01_DNS_SUFFIX) - 1; + apr_status_t rv; + + slen = strlen(servername); + if (slen <= sufflen + || apr_strnatcasecmp(MD_TLSSNI01_DNS_SUFFIX, servername + slen - sufflen)) { + return 0; + } + + sc = md_config_get(c->base_server); + if (sc && sc->mc->reg) { + md_store_t *store = md_reg_store_get(sc->mc->reg); + md_cert_t *mdcert; + md_pkey_t *mdpkey; + + rv = md_store_load(store, MD_SG_CHALLENGES, servername, + MD_FN_TLSSNI01_CERT, MD_SV_CERT, (void**)&mdcert, c->pool); + if (APR_SUCCESS == rv && (*pcert = md_cert_get_X509(mdcert))) { + rv = md_store_load(store, MD_SG_CHALLENGES, servername, + MD_FN_TLSSNI01_PKEY, MD_SV_PKEY, (void**)&mdpkey, c->pool); + if (APR_SUCCESS == rv && (*pkey = md_pkey_get_EVP_PKEY(mdpkey))) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, c, APLOGNO(10078) + "%s: is a tls-sni-01 challenge host", servername); + return 1; + } + ap_log_cerror(APLOG_MARK, APLOG_WARNING, rv, c, APLOGNO(10079) + "%s: challenge data not complete, key unavailable", servername); + } + else { + ap_log_cerror(APLOG_MARK, APLOG_INFO, rv, c, APLOGNO(10080) + "%s: unknown TLS SNI challenge host", servername); + } + } + *pcert = NULL; + *pkey = NULL; + return 0; +} + +/**************************************************************************************************/ +/* ACME challenge responses */ + +#define WELL_KNOWN_PREFIX "/.well-known/" +#define ACME_CHALLENGE_PREFIX WELL_KNOWN_PREFIX"acme-challenge/" + +static int md_http_challenge_pr(request_rec *r) +{ + apr_bucket_brigade *bb; + const md_srv_conf_t *sc; + const char *name, *data; + md_reg_t *reg; + int configured; + apr_status_t rv; + + if (r->parsed_uri.path + && !strncmp(ACME_CHALLENGE_PREFIX, r->parsed_uri.path, sizeof(ACME_CHALLENGE_PREFIX)-1)) { + sc = ap_get_module_config(r->server->module_config, &md_module); + if (sc && sc->mc) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "access inside /.well-known/acme-challenge for %s%s", + r->hostname, r->parsed_uri.path); + configured = (NULL != md_get_by_domain(sc->mc->mds, r->hostname)); + name = r->parsed_uri.path + sizeof(ACME_CHALLENGE_PREFIX)-1; + reg = sc && sc->mc? sc->mc->reg : NULL; + + if (strlen(name) && !ap_strchr_c(name, '/') && reg) { + md_store_t *store = md_reg_store_get(reg); + + rv = md_store_load(store, MD_SG_CHALLENGES, r->hostname, + MD_FN_HTTP01, MD_SV_TEXT, (void**)&data, r->pool); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r, + "loading challenge for %s (%s)", r->hostname, r->uri); + if (APR_SUCCESS == rv) { + apr_size_t len = strlen(data); + + if (r->method_number != M_GET) { + return HTTP_NOT_IMPLEMENTED; + } + /* A GET on a challenge resource for a hostname we are + * configured for. Let's send the content back */ + r->status = HTTP_OK; + apr_table_setn(r->headers_out, "Content-Length", apr_ltoa(r->pool, (long)len)); + + bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + apr_brigade_write(bb, NULL, NULL, data, len); + ap_pass_brigade(r->output_filters, bb); + apr_brigade_cleanup(bb); + + return DONE; + } + else if (!configured) { + /* The request hostname is not for a configured domain. We are not + * the sole authority here for /.well-known/acme-challenge (see PR62189). + * So, we decline to handle this and let others step in. + */ + return DECLINED; + } + else if (APR_STATUS_IS_ENOENT(rv)) { + return HTTP_NOT_FOUND; + } + else { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(10081) + "loading challenge %s from store", name); + return HTTP_INTERNAL_SERVER_ERROR; + } + } + } + } + return DECLINED; +} + +/**************************************************************************************************/ +/* Require Https hook */ + +static int md_require_https_maybe(request_rec *r) +{ + const md_srv_conf_t *sc; + apr_uri_t uri; + const char *s; + int status; + + if (opt_ssl_is_https && r->parsed_uri.path + && strncmp(WELL_KNOWN_PREFIX, r->parsed_uri.path, sizeof(WELL_KNOWN_PREFIX)-1)) { + + sc = ap_get_module_config(r->server->module_config, &md_module); + if (sc && sc->assigned && sc->assigned->require_https > MD_REQUIRE_OFF) { + if (opt_ssl_is_https(r->connection)) { + /* Using https: + * if 'permanent' and no one else set a HSTS header already, do it */ + if (sc->assigned->require_https == MD_REQUIRE_PERMANENT + && sc->mc->hsts_header && !apr_table_get(r->headers_out, MD_HSTS_HEADER)) { + apr_table_setn(r->headers_out, MD_HSTS_HEADER, sc->mc->hsts_header); + } + } + else { + /* Not using https:, but require it. Redirect. */ + if (r->method_number == M_GET) { + /* safe to use the old-fashioned codes */ + status = ((MD_REQUIRE_PERMANENT == sc->assigned->require_https)? + HTTP_MOVED_PERMANENTLY : HTTP_MOVED_TEMPORARILY); + } + else { + /* these should keep the method unchanged on retry */ + status = ((MD_REQUIRE_PERMANENT == sc->assigned->require_https)? + HTTP_PERMANENT_REDIRECT : HTTP_TEMPORARY_REDIRECT); + } + + s = ap_construct_url(r->pool, r->uri, r); + if (APR_SUCCESS == apr_uri_parse(r->pool, s, &uri)) { + uri.scheme = (char*)"https"; + uri.port = 443; + uri.port_str = (char*)"443"; + uri.query = r->parsed_uri.query; + uri.fragment = r->parsed_uri.fragment; + s = apr_uri_unparse(r->pool, &uri, APR_URI_UNP_OMITUSERINFO); + if (s && *s) { + apr_table_setn(r->headers_out, "Location", s); + return status; + } + } + } + } + } + return DECLINED; +} + +/* Runs once per created child process. Perform any process + * related initialization here. + */ +static void md_child_init(apr_pool_t *pool, server_rec *s) +{ + (void)pool; + (void)s; +} + +/* Install this module into the apache2 infrastructure. + */ +static void md_hooks(apr_pool_t *pool) +{ + static const char *const mod_ssl[] = { "mod_ssl.c", NULL}; + + md_acme_init(pool, AP_SERVER_BASEVERSION); + + ap_log_perror(APLOG_MARK, APLOG_TRACE1, 0, pool, "installing hooks"); + + /* Run once after configuration is set, before mod_ssl. + */ + ap_hook_post_config(md_post_config, NULL, mod_ssl, APR_HOOK_MIDDLE); + + /* Run once after a child process has been created. + */ + ap_hook_child_init(md_child_init, NULL, mod_ssl, APR_HOOK_MIDDLE); + + /* answer challenges *very* early, before any configured authentication may strike */ + ap_hook_post_read_request(md_require_https_maybe, NULL, NULL, APR_HOOK_FIRST); + ap_hook_post_read_request(md_http_challenge_pr, NULL, NULL, APR_HOOK_MIDDLE); + + APR_REGISTER_OPTIONAL_FN(md_is_managed); + APR_REGISTER_OPTIONAL_FN(md_get_certificate); + APR_REGISTER_OPTIONAL_FN(md_is_challenge); + APR_REGISTER_OPTIONAL_FN(md_get_credentials); +} + diff --git a/modules/md/mod_md.dep b/modules/md/mod_md.dep new file mode 100644 index 0000000..0cbd691 --- /dev/null +++ b/modules/md/mod_md.dep @@ -0,0 +1,5 @@ +# Microsoft Developer Studio Generated Dependency File, included by mod_md.mak
+
+..\..\build\win32\httpd.rc : \
+ "..\..\include\ap_release.h"\
+
diff --git a/modules/md/mod_md.dsp b/modules/md/mod_md.dsp new file mode 100644 index 0000000..c685f54 --- /dev/null +++ b/modules/md/mod_md.dsp @@ -0,0 +1,180 @@ +# Microsoft Developer Studio Project File - Name="mod_md" - Package Owner=<4>
+# Microsoft Developer Studio Generated Build File, Format Version 6.00
+# ** DO NOT EDIT **
+
+# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102
+
+CFG=mod_md - Win32 Release
+!MESSAGE This is not a valid makefile. To build this project using NMAKE,
+!MESSAGE use the Export Makefile command and run
+!MESSAGE
+!MESSAGE NMAKE /f "mod_md.mak".
+!MESSAGE
+!MESSAGE You can specify a configuration when running NMAKE
+!MESSAGE by defining the macro CFG on the command line. For example:
+!MESSAGE
+!MESSAGE NMAKE /f "mod_md.mak" CFG="mod_md - Win32 Release"
+!MESSAGE
+!MESSAGE Possible choices for configuration are:
+!MESSAGE
+!MESSAGE "mod_md - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library")
+!MESSAGE "mod_md - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library")
+!MESSAGE
+
+# Begin Project
+# PROP AllowPerConfigDependencies 0
+# PROP Scc_ProjName ""
+# PROP Scc_LocalPath ""
+CPP=cl.exe
+MTL=midl.exe
+RSC=rc.exe
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+# PROP BASE Use_MFC 0
+# PROP BASE Use_Debug_Libraries 0
+# PROP BASE Output_Dir "Release"
+# PROP BASE Intermediate_Dir "Release"
+# PROP BASE Target_Dir ""
+# PROP Use_MFC 0
+# PROP Use_Debug_Libraries 0
+# PROP Output_Dir "Release"
+# PROP Intermediate_Dir "Release"
+# PROP Ignore_Export_Lib 0
+# PROP Target_Dir ""
+# ADD BASE CPP /nologo /MD /W3 /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "ssize_t=long" /FD /c
+# ADD CPP /nologo /MD /W3 /O2 /Oy- /Zi /I "../../server/mpm/winnt" "/I ../ssl" /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" /I "../../srclib/openssl/inc32" /I "../../srclib/jansson/include" /I "../../srclib/curl/include" /I "../core" /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /D "ssize_t=long" /Fd"Release\mod_md_src" /FD /c
+# ADD BASE MTL /nologo /D "NDEBUG" /win32
+# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32
+# ADD BASE RSC /l 0x409 /d "NDEBUG"
+# ADD RSC /l 0x409 /fo"Release/mod_md.res" /i "../../include" /i "../../srclib/apr/include" /d "NDEBUG" /d "BIN_NAME=mod_md.so" /d "LONG_NAME=Letsencrypt module for Apache"
+BSC32=bscmake.exe
+# ADD BASE BSC32 /nologo
+# ADD BSC32 /nologo
+LINK32=link.exe
+# ADD BASE LINK32 kernel32.lib /nologo /subsystem:windows /dll /out:".\Release\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so
+# ADD LINK32 kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib ssleay32.lib libeay32.lib jansson.lib libcurl.lib /libpath:"../../srclib/apr/Release" /libpath:"../../srclib/apr-util/Release" /libpath:"../../Release/" /libpath:"../../srclib/openssl/out32dll" /libpath:"../../srclib/jansson/lib" /libpath:"../../srclib/curl/lib" /nologo /subsystem:windows /dll /incremental:no /debug /out:".\Release\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so /opt:ref
+# Begin Special Build Tool
+TargetPath=.\Release\mod_md.so
+SOURCE="$(InputPath)"
+PostBuild_Desc=Embed .manifest
+PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).manifest -outputresource:$(TargetPath);2
+# End Special Build Tool
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+# PROP BASE Use_MFC 0
+# PROP BASE Use_Debug_Libraries 1
+# PROP BASE Output_Dir "Debug"
+# PROP BASE Intermediate_Dir "Debug"
+# PROP BASE Target_Dir ""
+# PROP Use_MFC 0
+# PROP Use_Debug_Libraries 1
+# PROP Output_Dir "Debug"
+# PROP Intermediate_Dir "Debug"
+# PROP Ignore_Export_Lib 0
+# PROP Target_Dir ""
+# ADD BASE CPP /nologo /MDd /W3 /EHsc /Zi /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "ssize_t=long" /FD /c
+# ADD CPP /nologo /MDd /W3 /EHsc /Zi /Od /I "../ssl" /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" /I "../../srclib/openssl/inc32" /I "../../srclib/jansson/include" /I "../../srclib/curl/include" /I "../core" /src" /D "_DEBUG" /D "WIN32" /D "_WINDOWS" /D "ssize_t=long" /Fd"Debug\mod_md_src" /FD /c
+# ADD BASE MTL /nologo /D "_DEBUG" /win32
+# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32
+# ADD BASE RSC /l 0x409 /d "_DEBUG"
+# ADD RSC /l 0x409 /fo"Debug/mod_md.res" /i "../../include" /i "../../srclib/apr/include" /d "_DEBUG" /d "BIN_NAME=mod_md.so" /d "LONG_NAME=md_module for Apache"
+BSC32=bscmake.exe
+# ADD BASE BSC32 /nologo
+# ADD BSC32 /nologo
+LINK32=link.exe
+# ADD BASE LINK32 kernel32.lib /nologo /subsystem:windows /dll /incremental:no /debug /out:".\Debug\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so
+# ADD LINK32 kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib ssleay32.lib libeay32.lib jansson_d.lib libcurl_debug.lib /nologo /subsystem:windows /dll /libpath:"../../srclib/openssl/out32dll" /libpath:"../../srclib/jansson/lib" /libpath:"../../srclib/curl/lib" /incremental:no /debug /out:".\Debug\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so
+# Begin Special Build Tool
+TargetPath=.\Debug\mod_md.so
+SOURCE="$(InputPath)"
+PostBuild_Desc=Embed .manifest
+PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).manifest -outputresource:$(TargetPath);2
+# End Special Build Tool
+
+!ENDIF
+
+# Begin Target
+
+# Name "mod_md - Win32 Release"
+# Name "mod_md - Win32 Debug"
+# Begin Source File
+
+SOURCE=./mod_md.c
+# End Source File
+# Begin Source File
+
+SOURCE=./mod_md_config.c
+# End Source File
+# Begin Source File
+
+SOURCE=./mod_md_os.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_core.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_crypt.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_curl.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_http.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_json.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_jws.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_log.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_reg.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_store.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_store_fs.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_util.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_acme.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_acme_acct.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_acme_authz.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_acme_drive.c
+# End Source File
+# Begin Source File
+
+
+SOURCE=..\..\build\win32\httpd.rc
+# End Source File
+# End Target
+# End Project
diff --git a/modules/md/mod_md.h b/modules/md/mod_md.h new file mode 100644 index 0000000..5ff8f52 --- /dev/null +++ b/modules/md/mod_md.h @@ -0,0 +1,50 @@ +/* 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. + */ + +#ifndef mod_md_mod_md_h +#define mod_md_mod_md_h + +#include <openssl/evp.h> +#include <openssl/x509v3.h> + +struct server_rec; + +APR_DECLARE_OPTIONAL_FN(int, + md_is_managed, (struct server_rec *)); + +/** + * Get the certificate/key for the managed domain (md_is_managed != 0). + * + * @return APR_EAGAIN if the real certificate is not available yet + */ +APR_DECLARE_OPTIONAL_FN(apr_status_t, + md_get_certificate, (struct server_rec *, apr_pool_t *, + const char **pkeyfile, + const char **pcertfile)); + +APR_DECLARE_OPTIONAL_FN(int, + md_is_challenge, (struct conn_rec *, const char *, + X509 **pcert, EVP_PKEY **pkey)); + +/* Backward compatibility to older mod_ssl patches, will generate + * a WARNING in the logs, use 'md_get_certificate' instead */ +APR_DECLARE_OPTIONAL_FN(apr_status_t, + md_get_credentials, (struct server_rec *, apr_pool_t *, + const char **pkeyfile, + const char **pcertfile, + const char **pchainfile)); + +#endif /* mod_md_mod_md_h */ diff --git a/modules/md/mod_md.mak b/modules/md/mod_md.mak new file mode 100644 index 0000000..9d5881e --- /dev/null +++ b/modules/md/mod_md.mak @@ -0,0 +1,520 @@ +# Microsoft Developer Studio Generated NMAKE File, Based on mod_md.dsp
+!IF "$(CFG)" == ""
+CFG=mod_md - Win32 Release
+!MESSAGE No configuration specified. Defaulting to mod_md - Win32 Release.
+!ENDIF
+
+!IF "$(CFG)" != "mod_md - Win32 Release" && "$(CFG)" != "mod_md - Win32 Debug"
+!MESSAGE Invalid configuration "$(CFG)" specified.
+!MESSAGE You can specify a configuration when running NMAKE
+!MESSAGE by defining the macro CFG on the command line. For example:
+!MESSAGE
+!MESSAGE NMAKE /f "mod_md.mak" CFG="mod_md - Win32 Release"
+!MESSAGE
+!MESSAGE Possible choices for configuration are:
+!MESSAGE
+!MESSAGE "mod_md - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library")
+!MESSAGE "mod_md - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library")
+!MESSAGE
+!ERROR An invalid configuration is specified.
+!ENDIF
+
+!IF "$(OS)" == "Windows_NT"
+NULL=
+!ELSE
+NULL=nul
+!ENDIF
+
+!IF "$(_HAVE_OSSL110)" == "1"
+SSLCRP=libcrypto
+SSLLIB=libssl
+SSLINC=/I ../../srclib/openssl/include
+SSLBIN=/libpath:../../srclib/openssl
+!ELSE
+SSLCRP=libeay32
+SSLLIB=ssleay32
+SSLINC=/I ../../srclib/openssl/inc32
+SSLBIN=/libpath:../../srclib/openssl/out32dll
+!ENDIF
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+OUTDIR=.\Release
+INTDIR=.\Release
+# Begin Custom Macros
+OutDir=.\Release
+# End Custom Macros
+
+!IF "$(RECURSE)" == "0"
+
+ALL : "$(OUTDIR)\mod_md.so"
+
+!ELSE
+
+ALL : "libhttpd - Win32 Release" "libaprutil - Win32 Release" "libapr - Win32 Release" "$(OUTDIR)\mod_md.so"
+
+!ENDIF
+
+!IF "$(RECURSE)" == "1"
+CLEAN :"libapr - Win32 ReleaseCLEAN" "libaprutil - Win32 ReleaseCLEAN" "libhttpd - Win32 ReleaseCLEAN"
+!ELSE
+CLEAN :
+!ENDIF
+ -@erase "$(INTDIR)\md_acme.obj"
+ -@erase "$(INTDIR)\md_acme_acct.obj"
+ -@erase "$(INTDIR)\md_acme_authz.obj"
+ -@erase "$(INTDIR)\md_acme_drive.obj"
+ -@erase "$(INTDIR)\md_core.obj"
+ -@erase "$(INTDIR)\md_crypt.obj"
+ -@erase "$(INTDIR)\md_curl.obj"
+ -@erase "$(INTDIR)\md_http.obj"
+ -@erase "$(INTDIR)\md_json.obj"
+ -@erase "$(INTDIR)\md_jws.obj"
+ -@erase "$(INTDIR)\md_log.obj"
+ -@erase "$(INTDIR)\md_reg.obj"
+ -@erase "$(INTDIR)\md_store.obj"
+ -@erase "$(INTDIR)\md_store_fs.obj"
+ -@erase "$(INTDIR)\md_util.obj"
+ -@erase "$(INTDIR)\mod_md.obj"
+ -@erase "$(INTDIR)\mod_md.res"
+ -@erase "$(INTDIR)\mod_md_config.obj"
+ -@erase "$(INTDIR)\mod_md_os.obj"
+ -@erase "$(INTDIR)\mod_md_src.idb"
+ -@erase "$(INTDIR)\mod_md_src.pdb"
+ -@erase "$(OUTDIR)\mod_md.exp"
+ -@erase "$(OUTDIR)\mod_md.lib"
+ -@erase "$(OUTDIR)\mod_md.pdb"
+ -@erase "$(OUTDIR)\mod_md.so"
+
+"$(OUTDIR)" :
+ if not exist "$(OUTDIR)/$(NULL)" mkdir "$(OUTDIR)"
+
+CPP=cl.exe
+CPP_PROJ=/nologo /MD /W3 /Zi /O2 /Oy- /I "../../server/mpm/winnt" /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" $(SSLINC) /I "../../srclib/jansson/include" /I "../../srclib/curl/include" /I "../ssl" /I "../core" /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /D ssize_t=long /Fo"$(INTDIR)\\" /Fd"$(INTDIR)\mod_md_src" /FD /I " ../ssl" /c
+
+.c{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cpp{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cxx{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.c{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cpp{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cxx{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+MTL=midl.exe
+MTL_PROJ=/nologo /D "NDEBUG" /mktyplib203 /win32
+RSC=rc.exe
+RSC_PROJ=/l 0x409 /fo"$(INTDIR)\mod_md.res" /i "../../include" /i "../../srclib/apr/include" /d "NDEBUG" /d BIN_NAME=mod_md.so /d LONG_NAME=Letsencrypt module for Apache
+BSC32=bscmake.exe
+BSC32_FLAGS=/nologo /o"$(OUTDIR)\mod_md.bsc"
+BSC32_SBRS= \
+
+LINK32=link.exe
+LINK32_FLAGS=kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib $(SSLCRP).lib $(SSLLIB).lib jansson.lib libcurl.lib /nologo /subsystem:windows /dll /incremental:no /pdb:"$(OUTDIR)\mod_md.pdb" /debug /out:"$(OUTDIR)\mod_md.so" /implib:"$(OUTDIR)\mod_md.lib" /libpath:"../../srclib/apr/Release" /libpath:"../../srclib/apr-util/Release" /libpath:"../../Release/" $(SSLBIN) /libpath:"../../srclib/jansson/lib" /libpath:"../../srclib/curl/lib" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so /opt:ref
+LINK32_OBJS= \
+ "$(INTDIR)\mod_md.obj" \
+ "$(INTDIR)\mod_md_config.obj" \
+ "$(INTDIR)\mod_md_os.obj" \
+ "$(INTDIR)\md_core.obj" \
+ "$(INTDIR)\md_crypt.obj" \
+ "$(INTDIR)\md_curl.obj" \
+ "$(INTDIR)\md_http.obj" \
+ "$(INTDIR)\md_json.obj" \
+ "$(INTDIR)\md_jws.obj" \
+ "$(INTDIR)\md_log.obj" \
+ "$(INTDIR)\md_reg.obj" \
+ "$(INTDIR)\md_store.obj" \
+ "$(INTDIR)\md_store_fs.obj" \
+ "$(INTDIR)\md_util.obj" \
+ "$(INTDIR)\md_acme.obj" \
+ "$(INTDIR)\md_acme_acct.obj" \
+ "$(INTDIR)\md_acme_authz.obj" \
+ "$(INTDIR)\md_acme_drive.obj" \
+ "$(INTDIR)\mod_md.res" \
+ "..\..\srclib\apr\Release\libapr-1.lib" \
+ "..\..\srclib\apr-util\Release\libaprutil-1.lib" \
+ "..\..\Release\libhttpd.lib"
+
+"$(OUTDIR)\mod_md.so" : "$(OUTDIR)" $(DEF_FILE) $(LINK32_OBJS)
+ $(LINK32) @<<
+ $(LINK32_FLAGS) $(LINK32_OBJS)
+<<
+
+TargetPath=.\Release\mod_md.so
+SOURCE="$(InputPath)"
+PostBuild_Desc=Embed .manifest
+DS_POSTBUILD_DEP=$(INTDIR)\postbld.dep
+
+ALL : $(DS_POSTBUILD_DEP)
+
+# Begin Custom Macros
+OutDir=.\Release
+# End Custom Macros
+
+$(DS_POSTBUILD_DEP) : "libhttpd - Win32 Release" "libaprutil - Win32 Release" "libapr - Win32 Release" "$(OUTDIR)\mod_md.so"
+ if exist .\Release\mod_md.so.manifest mt.exe -manifest .\Release\mod_md.so.manifest -outputresource:.\Release\mod_md.so;2
+ echo Helper for Post-build step > "$(DS_POSTBUILD_DEP)"
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+OUTDIR=.\Debug
+INTDIR=.\Debug
+# Begin Custom Macros
+OutDir=.\Debug
+# End Custom Macros
+
+!IF "$(RECURSE)" == "0"
+
+ALL : "$(OUTDIR)\mod_md.so"
+
+!ELSE
+
+ALL : "libhttpd - Win32 Debug" "libaprutil - Win32 Debug" "libapr - Win32 Debug" "$(OUTDIR)\mod_md.so"
+
+!ENDIF
+
+!IF "$(RECURSE)" == "1"
+CLEAN :"libapr - Win32 DebugCLEAN" "libaprutil - Win32 DebugCLEAN" "libhttpd - Win32 DebugCLEAN"
+!ELSE
+CLEAN :
+!ENDIF
+ -@erase "$(INTDIR)\md_acme.obj"
+ -@erase "$(INTDIR)\md_acme_acct.obj"
+ -@erase "$(INTDIR)\md_acme_authz.obj"
+ -@erase "$(INTDIR)\md_acme_drive.obj"
+ -@erase "$(INTDIR)\md_core.obj"
+ -@erase "$(INTDIR)\md_crypt.obj"
+ -@erase "$(INTDIR)\md_curl.obj"
+ -@erase "$(INTDIR)\md_http.obj"
+ -@erase "$(INTDIR)\md_json.obj"
+ -@erase "$(INTDIR)\md_jws.obj"
+ -@erase "$(INTDIR)\md_log.obj"
+ -@erase "$(INTDIR)\md_reg.obj"
+ -@erase "$(INTDIR)\md_store.obj"
+ -@erase "$(INTDIR)\md_store_fs.obj"
+ -@erase "$(INTDIR)\md_util.obj"
+ -@erase "$(INTDIR)\mod_md.obj"
+ -@erase "$(INTDIR)\mod_md.res"
+ -@erase "$(INTDIR)\mod_md_config.obj"
+ -@erase "$(INTDIR)\mod_md_os.obj"
+ -@erase "$(INTDIR)\mod_md_src.idb"
+ -@erase "$(INTDIR)\mod_md_src.pdb"
+ -@erase "$(OUTDIR)\mod_md.exp"
+ -@erase "$(OUTDIR)\mod_md.lib"
+ -@erase "$(OUTDIR)\mod_md.pdb"
+ -@erase "$(OUTDIR)\mod_md.so"
+
+"$(OUTDIR)" :
+ if not exist "$(OUTDIR)/$(NULL)" mkdir "$(OUTDIR)"
+
+CPP=cl.exe
+CPP_PROJ=/nologo /MDd /W3 /Zi /Od /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" $(SSLINC) /I "../../srclib/jansson/include" /I "../../srclib/curl/include" /I "../core" /I "../ssl" /D "_DEBUG" /D "WIN32" /D "_WINDOWS" /D ssize_t=long /Fo"$(INTDIR)\\" /Fd"$(INTDIR)\mod_md_src" /FD /EHsc /c
+
+.c{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cpp{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cxx{$(INTDIR)}.obj::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.c{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cpp{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+.cxx{$(INTDIR)}.sbr::
+ $(CPP) @<<
+ $(CPP_PROJ) $<
+<<
+
+MTL=midl.exe
+MTL_PROJ=/nologo /D "_DEBUG" /mktyplib203 /win32
+RSC=rc.exe
+RSC_PROJ=/l 0x409 /fo"$(INTDIR)\mod_md.res" /i "../../include" /i "../../srclib/apr/include" /d "_DEBUG" /d BIN_NAME=mod_md.so /d LONG_NAME=http2_module for Apache
+BSC32=bscmake.exe
+BSC32_FLAGS=/nologo /o"$(OUTDIR)\mod_md.bsc"
+BSC32_SBRS= \
+
+LINK32=link.exe
+LINK32_FLAGS=kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib $(SSLCRP).lib $(SSLLIB).lib jansson_d.lib libcurl_debug.lib /nologo /subsystem:windows /dll /incremental:no /pdb:"$(OUTDIR)\mod_md.pdb" /debug /out:"$(OUTDIR)\mod_md.so" /implib:"$(OUTDIR)\mod_md.lib" $(SSLBIN) /libpath:"../../srclib/jansson/lib" /libpath:"../../srclib/curl/lib" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so
+LINK32_OBJS= \
+ "$(INTDIR)\mod_md.obj" \
+ "$(INTDIR)\mod_md_config.obj" \
+ "$(INTDIR)\mod_md_os.obj" \
+ "$(INTDIR)\md_core.obj" \
+ "$(INTDIR)\md_crypt.obj" \
+ "$(INTDIR)\md_curl.obj" \
+ "$(INTDIR)\md_http.obj" \
+ "$(INTDIR)\md_json.obj" \
+ "$(INTDIR)\md_jws.obj" \
+ "$(INTDIR)\md_log.obj" \
+ "$(INTDIR)\md_reg.obj" \
+ "$(INTDIR)\md_store.obj" \
+ "$(INTDIR)\md_store_fs.obj" \
+ "$(INTDIR)\md_util.obj" \
+ "$(INTDIR)\md_acme.obj" \
+ "$(INTDIR)\md_acme_acct.obj" \
+ "$(INTDIR)\md_acme_authz.obj" \
+ "$(INTDIR)\md_acme_drive.obj" \
+ "$(INTDIR)\mod_md.res" \
+ "..\..\srclib\apr\Debug\libapr-1.lib" \
+ "..\..\srclib\apr-util\Debug\libaprutil-1.lib" \
+ "..\..\Debug\libhttpd.lib"
+
+"$(OUTDIR)\mod_md.so" : "$(OUTDIR)" $(DEF_FILE) $(LINK32_OBJS)
+ $(LINK32) @<<
+ $(LINK32_FLAGS) $(LINK32_OBJS)
+<<
+
+TargetPath=.\Debug\mod_md.so
+SOURCE="$(InputPath)"
+PostBuild_Desc=Embed .manifest
+DS_POSTBUILD_DEP=$(INTDIR)\postbld.dep
+
+ALL : $(DS_POSTBUILD_DEP)
+
+# Begin Custom Macros
+OutDir=.\Debug
+# End Custom Macros
+
+$(DS_POSTBUILD_DEP) : "libhttpd - Win32 Debug" "libaprutil - Win32 Debug" "libapr - Win32 Debug" "$(OUTDIR)\mod_md.so"
+ if exist .\Debug\mod_md.so.manifest mt.exe -manifest .\Debug\mod_md.so.manifest -outputresource:.\Debug\mod_md.so;2
+ echo Helper for Post-build step > "$(DS_POSTBUILD_DEP)"
+
+!ENDIF
+
+
+!IF "$(NO_EXTERNAL_DEPS)" != "1"
+!IF EXISTS("mod_md.dep")
+!INCLUDE "mod_md.dep"
+!ELSE
+!MESSAGE Warning: cannot find "mod_md.dep"
+!ENDIF
+!ENDIF
+
+
+!IF "$(CFG)" == "mod_md - Win32 Release" || "$(CFG)" == "mod_md - Win32 Debug"
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+"libapr - Win32 Release" :
+ cd "..\..\srclib\apr"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libapr.mak" CFG="libapr - Win32 Release"
+ cd "..\..\modules\md"
+
+"libapr - Win32 ReleaseCLEAN" :
+ cd "..\..\srclib\apr"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libapr.mak" CFG="libapr - Win32 Release" RECURSE=1 CLEAN
+ cd "..\..\modules\md"
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+"libapr - Win32 Debug" :
+ cd "..\..\srclib\apr"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libapr.mak" CFG="libapr - Win32 Debug"
+ cd "..\..\modules\md"
+
+"libapr - Win32 DebugCLEAN" :
+ cd "..\..\srclib\apr"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libapr.mak" CFG="libapr - Win32 Debug" RECURSE=1 CLEAN
+ cd "..\..\modules\md"
+
+!ENDIF
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+"libaprutil - Win32 Release" :
+ cd "..\..\srclib\apr-util"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libaprutil.mak" CFG="libaprutil - Win32 Release"
+ cd "..\..\modules\md"
+
+"libaprutil - Win32 ReleaseCLEAN" :
+ cd "..\..\srclib\apr-util"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libaprutil.mak" CFG="libaprutil - Win32 Release" RECURSE=1 CLEAN
+ cd "..\..\modules\md"
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+"libaprutil - Win32 Debug" :
+ cd "..\..\srclib\apr-util"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libaprutil.mak" CFG="libaprutil - Win32 Debug"
+ cd "..\..\modules\md"
+
+"libaprutil - Win32 DebugCLEAN" :
+ cd "..\..\srclib\apr-util"
+ $(MAKE) /$(MAKEFLAGS) /F ".\libaprutil.mak" CFG="libaprutil - Win32 Debug" RECURSE=1 CLEAN
+ cd "..\..\modules\md"
+
+!ENDIF
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+"libhttpd - Win32 Release" :
+ cd "..\.."
+ $(MAKE) /$(MAKEFLAGS) /F ".\libhttpd.mak" CFG="libhttpd - Win32 Release"
+ cd ".\modules\md"
+
+"libhttpd - Win32 ReleaseCLEAN" :
+ cd "..\.."
+ $(MAKE) /$(MAKEFLAGS) /F ".\libhttpd.mak" CFG="libhttpd - Win32 Release" RECURSE=1 CLEAN
+ cd ".\modules\md"
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+"libhttpd - Win32 Debug" :
+ cd "..\.."
+ $(MAKE) /$(MAKEFLAGS) /F ".\libhttpd.mak" CFG="libhttpd - Win32 Debug"
+ cd ".\modules\md"
+
+"libhttpd - Win32 DebugCLEAN" :
+ cd "..\.."
+ $(MAKE) /$(MAKEFLAGS) /F ".\libhttpd.mak" CFG="libhttpd - Win32 Debug" RECURSE=1 CLEAN
+ cd ".\modules\md"
+
+!ENDIF
+
+SOURCE=..\..\build\win32\httpd.rc
+
+!IF "$(CFG)" == "mod_md - Win32 Release"
+
+
+"$(INTDIR)\mod_md.res" : $(SOURCE) "$(INTDIR)"
+ $(RSC) /l 0x409 /fo"$(INTDIR)\mod_md.res" /i "../../include" /i "../../srclib/apr/include" /i "../../build\win32" /d "NDEBUG" /d BIN_NAME="mod_md.so" /d LONG_NAME="md_module for Apache" $(SOURCE)
+
+
+!ELSEIF "$(CFG)" == "mod_md - Win32 Debug"
+
+
+"$(INTDIR)\mod_md.res" : $(SOURCE) "$(INTDIR)"
+ $(RSC) /l 0x409 /fo"$(INTDIR)\mod_md.res" /i "../../include" /i "../../srclib/apr/include" /i "../../build\win32" /d "_DEBUG" /d BIN_NAME="mod_md.so" /d LONG_NAME="md_module for Apache" $(SOURCE)
+
+
+!ENDIF
+
+SOURCE=./md_acme.c
+
+"$(INTDIR)\md_acme.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_acme_acct.c
+
+"$(INTDIR)\md_acme_acct.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_acme_authz.c
+
+"$(INTDIR)\md_acme_authz.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_acme_drive.c
+
+"$(INTDIR)\md_acme_drive.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_core.c
+
+"$(INTDIR)\md_core.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_crypt.c
+
+"$(INTDIR)\md_crypt.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_curl.c
+
+"$(INTDIR)\md_curl.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_http.c
+
+"$(INTDIR)\md_http.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_json.c
+
+"$(INTDIR)\md_json.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_jws.c
+
+"$(INTDIR)\md_jws.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_log.c
+
+"$(INTDIR)\md_log.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_reg.c
+
+"$(INTDIR)\md_reg.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_store.c
+
+"$(INTDIR)\md_store.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_store_fs.c
+
+"$(INTDIR)\md_store_fs.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_util.c
+
+"$(INTDIR)\md_util.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./mod_md.c
+
+"$(INTDIR)\mod_md.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./mod_md_config.c
+
+"$(INTDIR)\mod_md_config.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./mod_md_os.c
+
+"$(INTDIR)\mod_md_os.obj" : $(SOURCE) "$(INTDIR)"
+
+
+
+!ENDIF
+
diff --git a/modules/md/mod_md_config.c b/modules/md/mod_md_config.c new file mode 100644 index 0000000..336a21b --- /dev/null +++ b/modules/md/mod_md_config.c @@ -0,0 +1,950 @@ +/* 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. + */ + +#include <assert.h> + +#include <apr_lib.h> +#include <apr_strings.h> + +#include <httpd.h> +#include <http_core.h> +#include <http_config.h> +#include <http_log.h> +#include <http_vhost.h> + +#include "md.h" +#include "md_crypt.h" +#include "md_util.h" +#include "mod_md_private.h" +#include "mod_md_config.h" + +#define MD_CMD_MD "MDomain" +#define MD_CMD_OLD_MD "ManagedDomain" +#define MD_CMD_MD_SECTION "<MDomainSet" +#define MD_CMD_MD_OLD_SECTION "<ManagedDomain" +#define MD_CMD_BASE_SERVER "MDBaseServer" +#define MD_CMD_CA "MDCertificateAuthority" +#define MD_CMD_CAAGREEMENT "MDCertificateAgreement" +#define MD_CMD_CACHALLENGES "MDCAChallenges" +#define MD_CMD_CAPROTO "MDCertificateProtocol" +#define MD_CMD_DRIVEMODE "MDDriveMode" +#define MD_CMD_MEMBER "MDMember" +#define MD_CMD_MEMBERS "MDMembers" +#define MD_CMD_MUSTSTAPLE "MDMustStaple" +#define MD_CMD_NOTIFYCMD "MDNotifyCmd" +#define MD_CMD_PORTMAP "MDPortMap" +#define MD_CMD_PKEYS "MDPrivateKeys" +#define MD_CMD_PROXY "MDHttpProxy" +#define MD_CMD_RENEWWINDOW "MDRenewWindow" +#define MD_CMD_REQUIREHTTPS "MDRequireHttps" +#define MD_CMD_STOREDIR "MDStoreDir" + +#define DEF_VAL (-1) + +/* Default settings for the global conf */ +static md_mod_conf_t defmc = { + NULL, + "md", + NULL, + NULL, + 80, + 443, + 0, + 0, + 0, + MD_HSTS_MAX_AGE_DEFAULT, + NULL, + NULL, + NULL, +}; + +/* Default server specific setting */ +static md_srv_conf_t defconf = { + "default", + NULL, + &defmc, + + 1, + MD_REQUIRE_OFF, + MD_DRIVE_AUTO, + 0, + NULL, + apr_time_from_sec(90 * MD_SECS_PER_DAY), /* If the cert lifetime were 90 days, renew */ + apr_time_from_sec(30 * MD_SECS_PER_DAY), /* 30 days before. Adjust to actual lifetime */ + MD_ACME_DEF_URL, + "ACME", + NULL, + NULL, + NULL, + NULL, +}; + +static md_mod_conf_t *mod_md_config; + +static apr_status_t cleanup_mod_config(void *dummy) +{ + (void)dummy; + mod_md_config = NULL; + return APR_SUCCESS; +} + +static md_mod_conf_t *md_mod_conf_get(apr_pool_t *pool, int create) +{ + if (mod_md_config) { + return mod_md_config; /* reused for lifetime of the pool */ + } + + if (create) { + mod_md_config = apr_pcalloc(pool, sizeof(*mod_md_config)); + memcpy(mod_md_config, &defmc, sizeof(*mod_md_config)); + mod_md_config->mds = apr_array_make(pool, 5, sizeof(const md_t *)); + mod_md_config->unused_names = apr_array_make(pool, 5, sizeof(const md_t *)); + + apr_pool_cleanup_register(pool, NULL, cleanup_mod_config, apr_pool_cleanup_null); + } + + return mod_md_config; +} + +#define CONF_S_NAME(s) (s && s->server_hostname? s->server_hostname : "default") + +static void srv_conf_props_clear(md_srv_conf_t *sc) +{ + sc->transitive = DEF_VAL; + sc->require_https = MD_REQUIRE_UNSET; + sc->drive_mode = DEF_VAL; + sc->must_staple = DEF_VAL; + sc->pkey_spec = NULL; + sc->renew_norm = DEF_VAL; + sc->renew_window = DEF_VAL; + sc->ca_url = NULL; + sc->ca_proto = NULL; + sc->ca_agreement = NULL; + sc->ca_challenges = NULL; +} + +static void srv_conf_props_copy(md_srv_conf_t *to, const md_srv_conf_t *from) +{ + to->transitive = from->transitive; + to->require_https = from->require_https; + to->drive_mode = from->drive_mode; + to->must_staple = from->must_staple; + to->pkey_spec = from->pkey_spec; + to->renew_norm = from->renew_norm; + to->renew_window = from->renew_window; + to->ca_url = from->ca_url; + to->ca_proto = from->ca_proto; + to->ca_agreement = from->ca_agreement; + to->ca_challenges = from->ca_challenges; +} + +static void srv_conf_props_apply(md_t *md, const md_srv_conf_t *from, apr_pool_t *p) +{ + if (from->require_https != MD_REQUIRE_UNSET) md->require_https = from->require_https; + if (from->transitive != DEF_VAL) md->transitive = from->transitive; + if (from->drive_mode != DEF_VAL) md->drive_mode = from->drive_mode; + if (from->must_staple != DEF_VAL) md->must_staple = from->must_staple; + if (from->pkey_spec) md->pkey_spec = from->pkey_spec; + if (from->renew_norm != DEF_VAL) md->renew_norm = from->renew_norm; + if (from->renew_window != DEF_VAL) md->renew_window = from->renew_window; + + if (from->ca_url) md->ca_url = from->ca_url; + if (from->ca_proto) md->ca_proto = from->ca_proto; + if (from->ca_agreement) md->ca_agreement = from->ca_agreement; + if (from->ca_challenges) md->ca_challenges = apr_array_copy(p, from->ca_challenges); +} + +void *md_config_create_svr(apr_pool_t *pool, server_rec *s) +{ + md_srv_conf_t *conf = (md_srv_conf_t *)apr_pcalloc(pool, sizeof(md_srv_conf_t)); + + conf->name = apr_pstrcat(pool, "srv[", CONF_S_NAME(s), "]", NULL); + conf->s = s; + conf->mc = md_mod_conf_get(pool, 1); + + srv_conf_props_clear(conf); + + return conf; +} + +static void *md_config_merge(apr_pool_t *pool, void *basev, void *addv) +{ + md_srv_conf_t *base = (md_srv_conf_t *)basev; + md_srv_conf_t *add = (md_srv_conf_t *)addv; + md_srv_conf_t *nsc; + char *name = apr_pstrcat(pool, "[", CONF_S_NAME(add->s), ", ", CONF_S_NAME(base->s), "]", NULL); + + nsc = (md_srv_conf_t *)apr_pcalloc(pool, sizeof(md_srv_conf_t)); + nsc->name = name; + nsc->mc = add->mc? add->mc : base->mc; + nsc->assigned = add->assigned? add->assigned : base->assigned; + + nsc->transitive = (add->transitive != DEF_VAL)? add->transitive : base->transitive; + nsc->require_https = (add->require_https != MD_REQUIRE_UNSET)? add->require_https : base->require_https; + nsc->drive_mode = (add->drive_mode != DEF_VAL)? add->drive_mode : base->drive_mode; + nsc->must_staple = (add->must_staple != DEF_VAL)? add->must_staple : base->must_staple; + nsc->pkey_spec = add->pkey_spec? add->pkey_spec : base->pkey_spec; + nsc->renew_window = (add->renew_norm != DEF_VAL)? add->renew_norm : base->renew_norm; + nsc->renew_window = (add->renew_window != DEF_VAL)? add->renew_window : base->renew_window; + + nsc->ca_url = add->ca_url? add->ca_url : base->ca_url; + nsc->ca_proto = add->ca_proto? add->ca_proto : base->ca_proto; + nsc->ca_agreement = add->ca_agreement? add->ca_agreement : base->ca_agreement; + nsc->ca_challenges = (add->ca_challenges? apr_array_copy(pool, add->ca_challenges) + : (base->ca_challenges? apr_array_copy(pool, base->ca_challenges) : NULL)); + nsc->current = NULL; + nsc->assigned = NULL; + + return nsc; +} + +void *md_config_merge_svr(apr_pool_t *pool, void *basev, void *addv) +{ + return md_config_merge(pool, basev, addv); +} + +static int inside_section(cmd_parms *cmd, const char *section) { + ap_directive_t *d; + for (d = cmd->directive->parent; d; d = d->parent) { + if (!ap_cstr_casecmp(d->directive, section)) { + return 1; + } + } + return 0; +} + +static int inside_md_section(cmd_parms *cmd) { + return (inside_section(cmd, MD_CMD_MD_SECTION) || inside_section(cmd, MD_CMD_MD_OLD_SECTION)); +} + +static const char *md_section_check(cmd_parms *cmd) { + if (!inside_md_section(cmd)) { + return apr_pstrcat(cmd->pool, cmd->cmd->name, " is only valid inside a '", + MD_CMD_MD_SECTION, "' context, not here", NULL); + } + return NULL; +} + +static void add_domain_name(apr_array_header_t *domains, const char *name, apr_pool_t *p) +{ + if (md_array_str_index(domains, name, 0, 0) < 0) { + APR_ARRAY_PUSH(domains, char *) = md_util_str_tolower(apr_pstrdup(p, name)); + } +} + +static const char *set_transitive(int *ptransitive, const char *value) +{ + if (!apr_strnatcasecmp("auto", value)) { + *ptransitive = 1; + return NULL; + } + else if (!apr_strnatcasecmp("manual", value)) { + *ptransitive = 0; + return NULL; + } + return "unknown value, use \"auto|manual\""; +} + +static const char *md_config_sec_start(cmd_parms *cmd, void *mconfig, const char *arg) +{ + md_srv_conf_t *sc; + md_srv_conf_t save; + const char *endp; + const char *err, *name; + apr_array_header_t *domains; + md_t *md; + int transitive = -1; + + (void)mconfig; + if ((err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + + sc = md_config_get(cmd->server); + endp = ap_strrchr_c(arg, '>'); + if (endp == NULL) { + return MD_CMD_MD_SECTION "> directive missing closing '>'"; + } + + arg = apr_pstrndup(cmd->pool, arg, (apr_size_t)(endp-arg)); + if (!arg || !*arg) { + return MD_CMD_MD_SECTION " > section must specify a unique domain name"; + } + + name = ap_getword_white(cmd->pool, &arg); + domains = apr_array_make(cmd->pool, 5, sizeof(const char *)); + add_domain_name(domains, name, cmd->pool); + while (*arg != '\0') { + name = ap_getword_white(cmd->pool, &arg); + if (NULL != set_transitive(&transitive, name)) { + add_domain_name(domains, name, cmd->pool); + } + } + + if (domains->nelts == 0) { + return "needs at least one domain name"; + } + + md = md_create(cmd->pool, domains); + if (transitive >= 0) { + md->transitive = transitive; + } + + /* Save the current settings in this srv_conf and apply+restore at the + * end of this section */ + memcpy(&save, sc, sizeof(save)); + srv_conf_props_clear(sc); + sc->current = md; + + if (NULL == (err = ap_walk_config(cmd->directive->first_child, cmd, cmd->context))) { + srv_conf_props_apply(md, sc, cmd->pool); + APR_ARRAY_PUSH(sc->mc->mds, const md_t *) = md; + } + + sc->current = NULL; + srv_conf_props_copy(sc, &save); + + return err; +} + +static const char *md_config_sec_add_members(cmd_parms *cmd, void *dc, + int argc, char *const argv[]) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + int i; + + (void)dc; + if (NULL != (err = md_section_check(cmd))) { + if (argc == 1) { + /* only these values are allowed outside a section */ + return set_transitive(&sc->transitive, argv[0]); + } + return err; + } + + assert(sc->current); + for (i = 0; i < argc; ++i) { + if (NULL != set_transitive(&sc->transitive, argv[i])) { + add_domain_name(sc->current->domains, argv[i], cmd->pool); + } + } + return NULL; +} + +static const char *md_config_set_names(cmd_parms *cmd, void *dc, + int argc, char *const argv[]) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + apr_array_header_t *domains = apr_array_make(cmd->pool, 5, sizeof(const char *)); + const char *err; + md_t *md; + int i, transitive = -1; + + (void)dc; + err = ap_check_cmd_context(cmd, NOT_IN_DIR_LOC_FILE); + if (err) { + return err; + } + + for (i = 0; i < argc; ++i) { + if (NULL != set_transitive(&transitive, argv[i])) { + add_domain_name(domains, argv[i], cmd->pool); + } + } + + if (domains->nelts == 0) { + return "needs at least one domain name"; + } + md = md_create(cmd->pool, domains); + + if (transitive >= 0) { + md->transitive = transitive; + } + + if (cmd->config_file) { + md->defn_name = cmd->config_file->name; + md->defn_line_number = cmd->config_file->line_number; + } + + APR_ARRAY_PUSH(sc->mc->mds, md_t *) = md; + + return NULL; +} + +static const char *md_config_set_ca(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)dc; + if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + sc->ca_url = value; + return NULL; +} + +static const char *md_config_set_ca_proto(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + + (void)dc; + if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + config->ca_proto = value; + return NULL; +} + +static const char *md_config_set_agreement(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + + (void)dc; + if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + config->ca_agreement = value; + return NULL; +} + +static const char *md_config_set_drive_mode(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + md_drive_mode_t drive_mode; + + (void)dc; + if (!apr_strnatcasecmp("auto", value) || !apr_strnatcasecmp("automatic", value)) { + drive_mode = MD_DRIVE_AUTO; + } + else if (!apr_strnatcasecmp("always", value)) { + drive_mode = MD_DRIVE_ALWAYS; + } + else if (!apr_strnatcasecmp("manual", value) || !apr_strnatcasecmp("stick", value)) { + drive_mode = MD_DRIVE_MANUAL; + } + else { + return apr_pstrcat(cmd->pool, "unknown MDDriveMode ", value, NULL); + } + + if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + config->drive_mode = drive_mode; + return NULL; +} + +static const char *md_config_set_must_staple(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + + (void)dc; + if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + + if (!apr_strnatcasecmp("off", value)) { + config->must_staple = 0; + } + else if (!apr_strnatcasecmp("on", value)) { + config->must_staple = 1; + } + else { + return apr_pstrcat(cmd->pool, "unknown '", value, + "', supported parameter values are 'on' and 'off'", NULL); + } + return NULL; +} + +static const char *md_config_set_base_server(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + + (void)dc; + if (!err) { + if (!apr_strnatcasecmp("off", value)) { + config->mc->manage_base_server = 0; + } + else if (!apr_strnatcasecmp("on", value)) { + config->mc->manage_base_server = 1; + } + else { + err = apr_pstrcat(cmd->pool, "unknown '", value, + "', supported parameter values are 'on' and 'off'", NULL); + } + } + return err; +} + +static const char *md_config_set_require_https(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + + (void)dc; + if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + + if (!apr_strnatcasecmp("off", value)) { + config->require_https = MD_REQUIRE_OFF; + } + else if (!apr_strnatcasecmp(MD_KEY_TEMPORARY, value)) { + config->require_https = MD_REQUIRE_TEMPORARY; + } + else if (!apr_strnatcasecmp(MD_KEY_PERMANENT, value)) { + config->require_https = MD_REQUIRE_PERMANENT; + } + else { + return apr_pstrcat(cmd->pool, "unknown '", value, + "', supported parameter values are 'temporary' and 'permanent'", NULL); + } + return NULL; +} + +static apr_status_t duration_parse(const char *value, apr_interval_time_t *ptimeout, + const char *def_unit) +{ + char *endp; + long funits = 1; + apr_status_t rv; + apr_int64_t n; + + n = apr_strtoi64(value, &endp, 10); + if (errno) { + return errno; + } + if (!endp || !*endp) { + if (strcmp(def_unit, "d") == 0) { + def_unit = "s"; + funits = MD_SECS_PER_DAY; + } + } + else if (endp == value) { + return APR_EINVAL; + } + else if (*endp == 'd') { + *ptimeout = apr_time_from_sec(n * MD_SECS_PER_DAY); + return APR_SUCCESS; + } + else { + def_unit = endp; + } + rv = ap_timeout_parameter_parse(value, ptimeout, def_unit); + if (APR_SUCCESS == rv && funits > 1) { + *ptimeout *= funits; + } + return rv; +} + +static apr_status_t percentage_parse(const char *value, int *ppercent) +{ + char *endp; + apr_int64_t n; + + n = apr_strtoi64(value, &endp, 10); + if (errno) { + return errno; + } + if (*endp == '%') { + if (n < 0 || n >= 100) { + return APR_BADARG; + } + *ppercent = (int)n; + return APR_SUCCESS; + } + return APR_EINVAL; +} + +static const char *md_config_set_renew_window(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + apr_interval_time_t timeout; + int percent = 0; + + (void)dc; + if (!inside_md_section(cmd) + && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + + /* Inspired by http_core.c */ + if (duration_parse(value, &timeout, "d") == APR_SUCCESS) { + config->renew_norm = 0; + config->renew_window = timeout; + return NULL; + } + else { + switch (percentage_parse(value, &percent)) { + case APR_SUCCESS: + config->renew_norm = apr_time_from_sec(100 * MD_SECS_PER_DAY); + config->renew_window = apr_time_from_sec(percent * MD_SECS_PER_DAY); + return NULL; + case APR_BADARG: + return "MDRenewWindow as percent must be less than 100"; + } + } + return "MDRenewWindow has unrecognized format"; +} + +static const char *md_config_set_proxy(cmd_parms *cmd, void *arg, const char *value) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + + if (err) { + return err; + } + md_util_abs_http_uri_check(cmd->pool, value, &err); + if (err) { + return err; + } + sc->mc->proxy_url = value; + (void)arg; + return NULL; +} + +static const char *md_config_set_store_dir(cmd_parms *cmd, void *arg, const char *value) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + + if (err) { + return err; + } + sc->mc->base_dir = value; + (void)arg; + return NULL; +} + +static const char *set_port_map(md_mod_conf_t *mc, const char *value) +{ + int net_port, local_port; + char *endp; + + net_port = (int)apr_strtoi64(value, &endp, 10); + if (errno) { + return "unable to parse first port number"; + } + if (!endp || *endp != ':') { + return "no ':' after first port number"; + } + ++endp; + if (*endp == '-') { + local_port = 0; + } + else { + local_port = (int)apr_strtoi64(endp, &endp, 10); + if (errno) { + return "unable to parse second port number"; + } + if (local_port <= 0 || local_port > 65535) { + return "invalid number for port map, must be in ]0,65535]"; + } + } + switch (net_port) { + case 80: + mc->local_80 = local_port; + break; + case 443: + mc->local_443 = local_port; + break; + default: + return "mapped port number must be 80 or 443"; + } + return NULL; +} + +static const char *md_config_set_port_map(cmd_parms *cmd, void *arg, + const char *v1, const char *v2) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + + (void)arg; + if (!err) { + err = set_port_map(sc->mc, v1); + } + if (!err && v2) { + err = set_port_map(sc->mc, v2); + } + return err; +} + +static const char *md_config_set_cha_tyes(cmd_parms *cmd, void *dc, + int argc, char *const argv[]) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + apr_array_header_t **pcha, *ca_challenges; + const char *err; + int i; + + (void)dc; + if (!inside_md_section(cmd) + && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + pcha = &config->ca_challenges; + + ca_challenges = *pcha; + if (!ca_challenges) { + *pcha = ca_challenges = apr_array_make(cmd->pool, 5, sizeof(const char *)); + } + for (i = 0; i < argc; ++i) { + APR_ARRAY_PUSH(ca_challenges, const char *) = argv[i]; + } + + return NULL; +} + +static const char *md_config_set_pkeys(cmd_parms *cmd, void *dc, + int argc, char *const argv[]) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err, *ptype; + apr_int64_t bits; + + (void)dc; + if (!inside_md_section(cmd) + && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + if (argc <= 0) { + return "needs to specify the private key type"; + } + + ptype = argv[0]; + if (!apr_strnatcasecmp("Default", ptype)) { + if (argc > 1) { + return "type 'Default' takes no parameter"; + } + if (!config->pkey_spec) { + config->pkey_spec = apr_pcalloc(cmd->pool, sizeof(*config->pkey_spec)); + } + config->pkey_spec->type = MD_PKEY_TYPE_DEFAULT; + return NULL; + } + else if (!apr_strnatcasecmp("RSA", ptype)) { + if (argc == 1) { + bits = MD_PKEY_RSA_BITS_DEF; + } + else if (argc == 2) { + bits = (int)apr_atoi64(argv[1]); + if (bits < MD_PKEY_RSA_BITS_MIN || bits >= INT_MAX) { + return apr_psprintf(cmd->pool, "must be %d or higher in order to be considered " + "safe. Too large a value will slow down everything. Larger then 4096 probably does " + "not make sense unless quantum cryptography really changes spin.", + MD_PKEY_RSA_BITS_MIN); + } + } + else { + return "key type 'RSA' has only one optional parameter, the number of bits"; + } + + if (!config->pkey_spec) { + config->pkey_spec = apr_pcalloc(cmd->pool, sizeof(*config->pkey_spec)); + } + config->pkey_spec->type = MD_PKEY_TYPE_RSA; + config->pkey_spec->params.rsa.bits = (unsigned int)bits; + return NULL; + } + return apr_pstrcat(cmd->pool, "unsupported private key type \"", ptype, "\"", NULL); +} + +static const char *md_config_set_notify_cmd(cmd_parms *cmd, void *mconfig, const char *arg) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + + if (err) { + return err; + } + sc->mc->notify_cmd = arg; + (void)mconfig; + return NULL; +} + +static const char *md_config_set_names_old(cmd_parms *cmd, void *dc, + int argc, char *const argv[]) +{ + ap_log_error( APLOG_MARK, APLOG_WARNING, 0, cmd->server, + "mod_md: directive 'ManagedDomain' is deprecated, replace with 'MDomain'."); + return md_config_set_names(cmd, dc, argc, argv); +} + +static const char *md_config_sec_start_old(cmd_parms *cmd, void *mconfig, const char *arg) +{ + ap_log_error( APLOG_MARK, APLOG_WARNING, 0, cmd->server, + "mod_md: directive '<ManagedDomain' is deprecated, replace with '<MDomainSet'."); + return md_config_sec_start(cmd, mconfig, arg); +} + +const command_rec md_cmds[] = { + AP_INIT_TAKE1( MD_CMD_CA, md_config_set_ca, NULL, RSRC_CONF, + "URL of CA issuing the certificates"), + AP_INIT_TAKE1( MD_CMD_CAAGREEMENT, md_config_set_agreement, NULL, RSRC_CONF, + "URL of CA Terms-of-Service agreement you accept"), + AP_INIT_TAKE_ARGV( MD_CMD_CACHALLENGES, md_config_set_cha_tyes, NULL, RSRC_CONF, + "A list of challenge types to be used."), + AP_INIT_TAKE1( MD_CMD_CAPROTO, md_config_set_ca_proto, NULL, RSRC_CONF, + "Protocol used to obtain/renew certificates"), + AP_INIT_TAKE1( MD_CMD_DRIVEMODE, md_config_set_drive_mode, NULL, RSRC_CONF, + "method of obtaining certificates for the managed domain"), + AP_INIT_TAKE_ARGV( MD_CMD_MD, md_config_set_names, NULL, RSRC_CONF, + "A group of server names with one certificate"), + AP_INIT_RAW_ARGS( MD_CMD_MD_SECTION, md_config_sec_start, NULL, RSRC_CONF, + "Container for a managed domain with common settings and certificate."), + AP_INIT_TAKE_ARGV( MD_CMD_MEMBER, md_config_sec_add_members, NULL, RSRC_CONF, + "Define domain name(s) part of the Managed Domain. Use 'auto' or " + "'manual' to enable/disable auto adding names from virtual hosts."), + AP_INIT_TAKE_ARGV( MD_CMD_MEMBERS, md_config_sec_add_members, NULL, RSRC_CONF, + "Define domain name(s) part of the Managed Domain. Use 'auto' or " + "'manual' to enable/disable auto adding names from virtual hosts."), + AP_INIT_TAKE1( MD_CMD_MUSTSTAPLE, md_config_set_must_staple, NULL, RSRC_CONF, + "Enable/Disable the Must-Staple flag for new certificates."), + AP_INIT_TAKE12( MD_CMD_PORTMAP, md_config_set_port_map, NULL, RSRC_CONF, + "Declare the mapped ports 80 and 443 on the local server. E.g. 80:8000 " + "to indicate that the server port 8000 is reachable as port 80 from the " + "internet. Use 80:- to indicate that port 80 is not reachable from " + "the outside."), + AP_INIT_TAKE_ARGV( MD_CMD_PKEYS, md_config_set_pkeys, NULL, RSRC_CONF, + "set the type and parameters for private key generation"), + AP_INIT_TAKE1( MD_CMD_PROXY, md_config_set_proxy, NULL, RSRC_CONF, + "URL of a HTTP(S) proxy to use for outgoing connections"), + AP_INIT_TAKE1( MD_CMD_STOREDIR, md_config_set_store_dir, NULL, RSRC_CONF, + "the directory for file system storage of managed domain data."), + AP_INIT_TAKE1( MD_CMD_RENEWWINDOW, md_config_set_renew_window, NULL, RSRC_CONF, + "Time length for renewal before certificate expires (defaults to days)"), + AP_INIT_TAKE1( MD_CMD_REQUIREHTTPS, md_config_set_require_https, NULL, RSRC_CONF, + "Redirect non-secure requests to the https: equivalent."), + AP_INIT_RAW_ARGS(MD_CMD_NOTIFYCMD, md_config_set_notify_cmd, NULL, RSRC_CONF, + "set the command and optional arguments to run when signup/renew of domain is complete."), + AP_INIT_TAKE1( MD_CMD_BASE_SERVER, md_config_set_base_server, NULL, RSRC_CONF, + "allow managing of base server outside virtual hosts."), + +/* This will disappear soon */ + AP_INIT_TAKE_ARGV( MD_CMD_OLD_MD, md_config_set_names_old, NULL, RSRC_CONF, + "Deprecated, replace with 'MDomain'."), + AP_INIT_RAW_ARGS( MD_CMD_MD_OLD_SECTION, md_config_sec_start_old, NULL, RSRC_CONF, + "Deprecated, replace with '<MDomainSet'."), +/* */ + + AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL) +}; + +apr_status_t md_config_post_config(server_rec *s, apr_pool_t *p) +{ + md_srv_conf_t *sc; + md_mod_conf_t *mc; + + sc = md_config_get(s); + mc = sc->mc; + + mc->hsts_header = NULL; + if (mc->hsts_max_age > 0) { + mc->hsts_header = apr_psprintf(p, "max-age=%d", mc->hsts_max_age); + } + + return APR_SUCCESS; +} + +static md_srv_conf_t *config_get_int(server_rec *s, apr_pool_t *p) +{ + md_srv_conf_t *sc = (md_srv_conf_t *)ap_get_module_config(s->module_config, &md_module); + ap_assert(sc); + if (sc->s != s && p) { + sc = md_config_merge(p, &defconf, sc); + sc->name = apr_pstrcat(p, CONF_S_NAME(s), sc->name, NULL); + sc->mc = md_mod_conf_get(p, 1); + ap_set_module_config(s->module_config, &md_module, sc); + } + return sc; +} + +md_srv_conf_t *md_config_get(server_rec *s) +{ + return config_get_int(s, NULL); +} + +md_srv_conf_t *md_config_get_unique(server_rec *s, apr_pool_t *p) +{ + assert(p); + return config_get_int(s, p); +} + +md_srv_conf_t *md_config_cget(conn_rec *c) +{ + return md_config_get(c->base_server); +} + +const char *md_config_gets(const md_srv_conf_t *sc, md_config_var_t var) +{ + switch (var) { + case MD_CONFIG_CA_URL: + return sc->ca_url? sc->ca_url : defconf.ca_url; + case MD_CONFIG_CA_PROTO: + return sc->ca_proto? sc->ca_proto : defconf.ca_proto; + case MD_CONFIG_BASE_DIR: + return sc->mc->base_dir; + case MD_CONFIG_PROXY: + return sc->mc->proxy_url; + case MD_CONFIG_CA_AGREEMENT: + return sc->ca_agreement? sc->ca_agreement : defconf.ca_agreement; + case MD_CONFIG_NOTIFY_CMD: + return sc->mc->notify_cmd; + default: + return NULL; + } +} + +int md_config_geti(const md_srv_conf_t *sc, md_config_var_t var) +{ + switch (var) { + case MD_CONFIG_DRIVE_MODE: + return (sc->drive_mode != DEF_VAL)? sc->drive_mode : defconf.drive_mode; + case MD_CONFIG_LOCAL_80: + return sc->mc->local_80; + case MD_CONFIG_LOCAL_443: + return sc->mc->local_443; + case MD_CONFIG_TRANSITIVE: + return (sc->transitive != DEF_VAL)? sc->transitive : defconf.transitive; + case MD_CONFIG_REQUIRE_HTTPS: + return (sc->require_https != MD_REQUIRE_UNSET)? sc->require_https : defconf.require_https; + case MD_CONFIG_MUST_STAPLE: + return (sc->must_staple != DEF_VAL)? sc->must_staple : defconf.must_staple; + default: + return 0; + } +} + +apr_interval_time_t md_config_get_interval(const md_srv_conf_t *sc, md_config_var_t var) +{ + switch (var) { + case MD_CONFIG_RENEW_NORM: + return (sc->renew_norm != DEF_VAL)? sc->renew_norm : defconf.renew_norm; + case MD_CONFIG_RENEW_WINDOW: + return (sc->renew_window != DEF_VAL)? sc->renew_window : defconf.renew_window; + default: + return 0; + } +} diff --git a/modules/md/mod_md_config.h b/modules/md/mod_md_config.h new file mode 100644 index 0000000..7c7df51 --- /dev/null +++ b/modules/md/mod_md_config.h @@ -0,0 +1,102 @@ +/* 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. + */ + +#ifndef mod_md_md_config_h +#define mod_md_md_config_h + +struct md_store_t; +struct md_reg_t; +struct md_pkey_spec_t; + +typedef enum { + MD_CONFIG_CA_URL, + MD_CONFIG_CA_PROTO, + MD_CONFIG_BASE_DIR, + MD_CONFIG_CA_AGREEMENT, + MD_CONFIG_DRIVE_MODE, + MD_CONFIG_LOCAL_80, + MD_CONFIG_LOCAL_443, + MD_CONFIG_RENEW_NORM, + MD_CONFIG_RENEW_WINDOW, + MD_CONFIG_TRANSITIVE, + MD_CONFIG_PROXY, + MD_CONFIG_REQUIRE_HTTPS, + MD_CONFIG_MUST_STAPLE, + MD_CONFIG_NOTIFY_CMD, +} md_config_var_t; + +typedef struct { + apr_array_header_t *mds; /* all md_t* defined in the config, shared */ + const char *base_dir; /* base dir for store */ + const char *proxy_url; /* proxy url to use (or NULL) */ + struct md_reg_t *reg; /* md registry instance, singleton, shared */ + + int local_80; /* On which port http:80 arrives */ + int local_443; /* On which port https:443 arrives */ + int can_http; /* Does someone listen to the local port 80 equivalent? */ + int can_https; /* Does someone listen to the local port 443 equivalent? */ + int manage_base_server; /* If base server outside vhost may be managed */ + int hsts_max_age; /* max-age of HSTS (rfc6797) header */ + const char *hsts_header; /* computed HTST header to use or NULL */ + apr_array_header_t *unused_names; /* post config, names of all MDs not assigned to a vhost */ + + const char *notify_cmd; /* notification command to execute on signup/renew */ +} md_mod_conf_t; + +typedef struct md_srv_conf_t { + const char *name; + const server_rec *s; /* server this config belongs to */ + md_mod_conf_t *mc; /* global config settings */ + + int transitive; /* != 0 iff VirtualHost names/aliases are auto-added */ + md_require_t require_https; /* If MDs require https: access */ + int drive_mode; /* mode of obtaining credentials */ + int must_staple; /* certificates should set the OCSP Must Staple extension */ + struct md_pkey_spec_t *pkey_spec; /* specification for generating private keys */ + apr_interval_time_t renew_norm; /* If > 0, use as normalizing value for cert lifetime + * Example: renew_norm=90d renew_win=30d, cert lives + * for 12 days => renewal 4 days before */ + apr_interval_time_t renew_window; /* time before expiration that starts renewal */ + + const char *ca_url; /* url of CA certificate service */ + const char *ca_proto; /* protocol used vs CA (e.g. ACME) */ + const char *ca_agreement; /* accepted agreement uri between CA and user */ + struct apr_array_header_t *ca_challenges; /* challenge types configured */ + + md_t *current; /* md currently defined in <MDomainSet xxx> section */ + md_t *assigned; /* post_config: MD that applies to this server or NULL */ +} md_srv_conf_t; + +void *md_config_create_svr(apr_pool_t *pool, server_rec *s); +void *md_config_merge_svr(apr_pool_t *pool, void *basev, void *addv); + +extern const command_rec md_cmds[]; + +apr_status_t md_config_post_config(server_rec *s, apr_pool_t *p); + +/* Get the effective md configuration for the connection */ +md_srv_conf_t *md_config_cget(conn_rec *c); +/* Get the effective md configuration for the server */ +md_srv_conf_t *md_config_get(server_rec *s); +/* Get the effective md configuration for the server, but make it + * unique to this server_rec, so that any changes only affect this server */ +md_srv_conf_t *md_config_get_unique(server_rec *s, apr_pool_t *p); + +const char *md_config_gets(const md_srv_conf_t *config, md_config_var_t var); +int md_config_geti(const md_srv_conf_t *config, md_config_var_t var); +apr_interval_time_t md_config_get_interval(const md_srv_conf_t *config, md_config_var_t var); + +#endif /* md_config_h */ diff --git a/modules/md/mod_md_os.c b/modules/md/mod_md_os.c new file mode 100644 index 0000000..f96d566 --- /dev/null +++ b/modules/md/mod_md_os.c @@ -0,0 +1,89 @@ +/* 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. + */ + +#include <assert.h> +#include <apr_strings.h> + +#ifndef AP_ENABLE_EXCEPTION_HOOK +#define AP_ENABLE_EXCEPTION_HOOK 0 +#endif + +#include <mpm_common.h> +#include <httpd.h> +#include <http_log.h> +#include <ap_mpm.h> + +#if APR_HAVE_UNISTD_H +#include <unistd.h> +#endif +#ifdef WIN32 +#include "mpm_winnt.h" +#endif +#if AP_NEED_SET_MUTEX_PERMS +#include "unixd.h" +#endif + +#include "md_util.h" +#include "mod_md_os.h" + +apr_status_t md_try_chown(const char *fname, unsigned int uid, int gid, apr_pool_t *p) +{ +#if AP_NEED_SET_MUTEX_PERMS + if (-1 == chown(fname, (uid_t)uid, (gid_t)gid)) { + apr_status_t rv = APR_FROM_OS_ERROR(errno); + if (!APR_STATUS_IS_ENOENT(rv)) { + ap_log_perror(APLOG_MARK, APLOG_ERR, rv, p, APLOGNO(10082) + "Can't change owner of %s", fname); + } + return rv; + } + return APR_SUCCESS; +#else + return APR_ENOTIMPL; +#endif +} + +apr_status_t md_make_worker_accessible(const char *fname, apr_pool_t *p) +{ +#if AP_NEED_SET_MUTEX_PERMS + return md_try_chown(fname, ap_unixd_config.user_id, -1, p); +#else + return APR_ENOTIMPL; +#endif +} + +#ifdef WIN32 + +apr_status_t md_server_graceful(apr_pool_t *p, server_rec *s) +{ + return APR_ENOTIMPL; +} + +#else + +apr_status_t md_server_graceful(apr_pool_t *p, server_rec *s) +{ + apr_status_t rv; + + (void)p; + (void)s; + rv = (kill(getppid(), AP_SIG_GRACEFUL) < 0)? APR_ENOTIMPL : APR_SUCCESS; + ap_log_error(APLOG_MARK, APLOG_TRACE1, errno, NULL, "sent signal to parent"); + return rv; +} + +#endif + diff --git a/modules/md/mod_md_os.h b/modules/md/mod_md_os.h new file mode 100644 index 0000000..3085076 --- /dev/null +++ b/modules/md/mod_md_os.h @@ -0,0 +1,37 @@ +/* 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. + */ + +#ifndef mod_md_md_os_h +#define mod_md_md_os_h + +/** + * Try chown'ing the file/directory. Give id -1 to not change uid/gid. + * Will return APR_ENOTIMPL on platforms not supporting this operation. + */ +apr_status_t md_try_chown(const char *fname, unsigned int uid, int gid, apr_pool_t *p); + +/** + * Make a file or directory read/write(/searchable) by httpd workers. + */ +apr_status_t md_make_worker_accessible(const char *fname, apr_pool_t *p); + +/** + * Trigger a graceful restart of the server. Depending on the architecture, may + * return APR_ENOTIMPL. + */ +apr_status_t md_server_graceful(apr_pool_t *p, server_rec *s); + +#endif /* mod_md_md_os_h */ diff --git a/modules/md/mod_md_private.h b/modules/md/mod_md_private.h new file mode 100644 index 0000000..45521ea --- /dev/null +++ b/modules/md/mod_md_private.h @@ -0,0 +1,24 @@ +/* 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. + */ + +#ifndef mod_md_md_private_h +#define mod_md_md_private_h + +extern module AP_MODULE_DECLARE_DATA md_module; + +APLOG_USE_MODULE(md); + +#endif |