diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-25 04:41:28 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-25 04:41:28 +0000 |
commit | b1a1c1d95059e2fefd7b5671eb110ab690409a84 (patch) | |
tree | 97ecfcc9425e2d09d2cd669594d626a616f324a3 /modules/md | |
parent | Releasing progress-linux version 2.4.38-3+deb10u10progress5u1. (diff) | |
download | apache2-b1a1c1d95059e2fefd7b5671eb110ab690409a84.tar.xz apache2-b1a1c1d95059e2fefd7b5671eb110ab690409a84.zip |
Merging upstream version 2.4.59.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'modules/md')
59 files changed, 15284 insertions, 4680 deletions
diff --git a/modules/md/config2.m4 b/modules/md/config2.m4 index a2c8303..11d4f32 100644 --- a/modules/md/config2.m4 +++ b/modules/md/config2.m4 @@ -99,7 +99,7 @@ AC_DEFUN([APACHE_CHECK_CURL],[ AC_CHECK_HEADERS([curl/curl.h]) - AC_MSG_CHECKING([for curl version >= 7.50]) + AC_MSG_CHECKING([for curl version >= 7.29]) AC_TRY_COMPILE([#include <curl/curlver.h>],[ #if !defined(LIBCURL_VERSION_MAJOR) #error "Missing libcurl version" @@ -107,7 +107,7 @@ AC_DEFUN([APACHE_CHECK_CURL],[ #if LIBCURL_VERSION_MAJOR < 7 #error "Unsupported libcurl version " LIBCURL_VERSION #endif -#if LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR < 50 +#if LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR < 29 #error "Unsupported libcurl version " LIBCURL_VERSION #endif], [AC_MSG_RESULT(OK) @@ -248,20 +248,31 @@ md_acme.lo dnl md_acme_acct.lo dnl md_acme_authz.lo dnl md_acme_drive.lo dnl +md_acmev2_drive.lo dnl +md_acme_order.lo dnl md_core.lo dnl md_curl.lo dnl md_crypt.lo dnl +md_event.lo dnl md_http.lo dnl md_json.lo dnl md_jws.lo dnl md_log.lo dnl +md_ocsp.lo dnl +md_result.lo dnl md_reg.lo dnl +md_status.lo dnl md_store.lo dnl md_store_fs.lo dnl +md_tailscale.lo dnl +md_time.lo dnl md_util.lo dnl mod_md.lo dnl mod_md_config.lo dnl +mod_md_drive.lo dnl +mod_md_ocsp.lo dnl mod_md_os.lo dnl +mod_md_status.lo dnl " # Ensure that other modules can pick up mod_md.h diff --git a/modules/md/md.h b/modules/md/md.h index 60f8852..035ccba 100644 --- a/modules/md/md.h +++ b/modules/md/md.h @@ -17,19 +17,22 @@ #ifndef mod_md_md_h #define mod_md_md_h +#include <apr_time.h> + +#include "md_time.h" #include "md_version.h" struct apr_array_header_t; struct apr_hash_t; struct md_json_t; struct md_cert_t; +struct md_job_t; struct md_pkey_t; +struct md_result_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 @@ -37,13 +40,22 @@ struct md_pkey_spec_t; #define MD_HSTS_HEADER "Strict-Transport-Security" #define MD_HSTS_MAX_AGE_DEFAULT 15768000 +#define PROTO_ACME_TLS_1 "acme-tls/1" + +#define MD_TIME_LIFE_NORM (apr_time_from_sec(100 * MD_SECS_PER_DAY)) +#define MD_TIME_RENEW_WINDOW_DEF (apr_time_from_sec(33 * MD_SECS_PER_DAY)) +#define MD_TIME_WARN_WINDOW_DEF (apr_time_from_sec(10 * MD_SECS_PER_DAY)) +#define MD_TIME_OCSP_KEEP_NORM (apr_time_from_sec(7 * MD_SECS_PER_DAY)) + +#define MD_OTHER "other" + 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_S_UNKNOWN = 0, /* MD has not been analysed yet */ + MD_S_INCOMPLETE = 1, /* MD is missing necessary information, cannot go live */ + MD_S_COMPLETE = 2, /* MD has all necessary information, can go live */ + MD_S_EXPIRED_DEPRECATED = 3, /* deprecated */ + MD_S_ERROR = 4, /* MD data is flawed, unable to be processed as is */ + MD_S_MISSING_INFORMATION = 5, /* User has not agreed to ToS */ } md_state_t; typedef enum { @@ -54,30 +66,11 @@ typedef enum { } 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; + MD_RENEW_DEFAULT = -1, /* default value */ + MD_RENEW_MANUAL, /* manually triggered renewal of certificate */ + MD_RENEW_AUTO, /* automatic process performed by httpd */ + MD_RENEW_ALWAYS, /* always renewed by httpd, even if not necessary */ +} md_renew_mode_t; typedef struct md_t md_t; struct md_t { @@ -85,90 +78,142 @@ struct md_t { 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 */ + struct md_pkeys_spec_t *pks; /* specification for generating private keys */ + md_timeslice_t *renew_window; /* time before expiration that starts renewal */ + md_timeslice_t *warn_window; /* time before expiration that warnings are sent out */ - const char *ca_url; /* url of CA certificate service */ const char *ca_proto; /* protocol used vs CA (e.g. ACME) */ + struct apr_array_header_t *ca_urls; /* urls of CAs */ + const char *ca_effective; /* url of CA used */ const char *ca_account; /* account used at CA */ - const char *ca_agreement; /* accepted agreement uri between CA and user */ + const char *ca_agreement; /* accepted agreement uri between CA and user */ struct apr_array_header_t *ca_challenges; /* challenge types configured for this MD */ + struct apr_array_header_t *cert_files; /* != NULL iff pubcerts explicitly configured */ + struct apr_array_header_t *pkey_files; /* != NULL iff privkeys explicitly configured */ + const char *ca_eab_kid; /* optional KEYID for external account binding */ + const char *ca_eab_hmac; /* optional HMAC for external account binding */ - 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 char *state_descr; /* description of state of NULL */ + struct apr_array_header_t *acme_tls_1_domains; /* domains supporting "acme-tls/1" protocol */ + const char *dns01_cmd; /* DNS challenge command, override global command */ + 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 */ + const char *configured_name; /* name this MD was configured with, if different */ + + int renew_mode; /* mode of obtaining credentials */ + md_require_t require_https; /* Iff https: is required for this MD */ + md_state_t state; /* state of this MD */ + int transitive; /* != 0 iff VirtualHost names/aliases are auto-added */ + int must_staple; /* certificates should set the OCSP Must Staple extension */ + int stapling; /* if OCSP stapling is enabled */ + int watched; /* if certificate is supervised (renew or expiration warning) */ }; #define MD_KEY_ACCOUNT "account" +#define MD_KEY_ACME_TLS_1 "acme-tls/1" +#define MD_KEY_ACTIVATION_DELAY "activation-delay" +#define MD_KEY_ACTIVITY "activity" #define MD_KEY_AGREEMENT "agreement" +#define MD_KEY_AUTHORIZATIONS "authorizations" #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_CERT_FILES "cert-files" +#define MD_KEY_CERTIFICATE "certificate" +#define MD_KEY_CHALLENGE "challenge" #define MD_KEY_CHALLENGES "challenges" +#define MD_KEY_CMD_DNS01 "cmd-dns-01" +#define MD_KEY_DNS01_VERSION "cmd-dns-01-version" +#define MD_KEY_COMPLETE "complete" #define MD_KEY_CONTACT "contact" #define MD_KEY_CONTACTS "contacts" #define MD_KEY_CSR "csr" +#define MD_KEY_CURVE "curve" #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_EAB "eab" +#define MD_KEY_EAB_REQUIRED "externalAccountRequired" +#define MD_KEY_ENTRIES "entries" +#define MD_KEY_ERRORED "errored" +#define MD_KEY_ERROR "error" #define MD_KEY_ERRORS "errors" #define MD_KEY_EXPIRES "expires" +#define MD_KEY_FINALIZE "finalize" +#define MD_KEY_FINISHED "finished" +#define MD_KEY_FROM "from" +#define MD_KEY_GOOD "good" +#define MD_KEY_HMAC "hmac" #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_KID "kid" #define MD_KEY_KEYAUTHZ "keyAuthorization" +#define MD_KEY_LAST "last" +#define MD_KEY_LAST_RUN "last-run" #define MD_KEY_LOCATION "location" +#define MD_KEY_LOG "log" +#define MD_KEY_MDS "managed-domains" +#define MD_KEY_MESSAGE "message" #define MD_KEY_MUST_STAPLE "must-staple" #define MD_KEY_NAME "name" +#define MD_KEY_NEXT_RUN "next-run" +#define MD_KEY_NOTIFIED "notified" +#define MD_KEY_NOTIFIED_RENEWED "notified-renewed" +#define MD_KEY_OCSP "ocsp" +#define MD_KEY_OCSPS "ocsps" +#define MD_KEY_ORDERS "orders" #define MD_KEY_PERMANENT "permanent" #define MD_KEY_PKEY "privkey" -#define MD_KEY_PROCESSED "processed" +#define MD_KEY_PKEY_FILES "pkey-files" +#define MD_KEY_PROBLEM "problem" #define MD_KEY_PROTO "proto" +#define MD_KEY_READY "ready" #define MD_KEY_REGISTRATION "registration" #define MD_KEY_RENEW "renew" +#define MD_KEY_RENEW_AT "renew-at" +#define MD_KEY_RENEW_MODE "renew-mode" +#define MD_KEY_RENEWAL "renewal" +#define MD_KEY_RENEWING "renewing" #define MD_KEY_RENEW_WINDOW "renew-window" #define MD_KEY_REQUIRE_HTTPS "require-https" #define MD_KEY_RESOURCE "resource" +#define MD_KEY_RESPONSE "response" +#define MD_KEY_REVOKED "revoked" +#define MD_KEY_SERIAL "serial" +#define MD_KEY_SHA256_FINGERPRINT "sha256-fingerprint" +#define MD_KEY_STAPLING "stapling" #define MD_KEY_STATE "state" +#define MD_KEY_STATE_DESCR "state-descr" #define MD_KEY_STATUS "status" #define MD_KEY_STORE "store" +#define MD_KEY_SUBPROBLEMS "subproblems" #define MD_KEY_TEMPORARY "temporary" +#define MD_KEY_TOS "termsOfService" #define MD_KEY_TOKEN "token" +#define MD_KEY_TOTAL "total" #define MD_KEY_TRANSITIVE "transitive" #define MD_KEY_TYPE "type" +#define MD_KEY_UNKNOWN "unknown" +#define MD_KEY_UNTIL "until" #define MD_KEY_URL "url" +#define MD_KEY_URLS "urls" #define MD_KEY_URI "uri" -#define MD_KEY_VALID_FROM "validFrom" +#define MD_KEY_VALID "valid" +#define MD_KEY_VALID_FROM "valid-from" #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" +#define MD_KEY_WATCHED "watched" +#define MD_KEY_WHEN "when" +#define MD_KEY_WARN_WINDOW "warn-window" /* Check if a string member of a new MD (n) has * a value and if it differs from the old MD o @@ -223,12 +268,6 @@ md_t *md_get_by_domain(struct apr_array_header_t *mds, const char *domain); 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); @@ -248,43 +287,44 @@ md_t *md_clone(apr_pool_t *p, const md_t *src); */ 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); +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) + * Same as md_to_json(), but with sensitive fields stripped. */ -int md_should_renew(const md_t *md); +struct md_json_t *md_to_public_json(const md_t *md, apr_pool_t *p); + +int md_is_covered_by_alt_names(const md_t *md, const struct apr_array_header_t* alt_names); + +/* how many certificates this domain has/will eventually have. */ +int md_cert_count(const md_t *md); + +const char *md_get_ca_name_from_url(apr_pool_t *p, const char *url); +apr_status_t md_get_ca_url_from_name(const char **purl, apr_pool_t *p, const char *name); + +/**************************************************************************************************/ +/* notifications */ + +typedef apr_status_t md_job_notify_cb(struct md_job_t *job, const char *reason, + struct md_result_t *result, apr_pool_t *p, void *baton); /**************************************************************************************************/ /* 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; +typedef struct md_pubcert_t md_pubcert_t; +struct md_pubcert_t { + struct apr_array_header_t *certs; /* chain of const md_cert*, leaf cert first */ + struct apr_array_header_t *alt_names; /* alt-names of leaf cert */ + const char *cert_file; /* file path of chain */ + const char *key_file; /* file path of key for leaf cert */ }; -/* 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) +#define MD_OK(c) (APR_SUCCESS == (rv = c)) #endif /* mod_md_md_h */ diff --git a/modules/md/md_acme.c b/modules/md/md_acme.c index 3fbd365..4366bf6 100644 --- a/modules/md/md_acme.c +++ b/modules/md/md_acme.c @@ -30,6 +30,7 @@ #include "md_http.h" #include "md_log.h" #include "md_store.h" +#include "md_result.h" #include "md_util.h" #include "md_version.h" @@ -37,34 +38,36 @@ #include "md_acme_acct.h" -static const char *base_product; +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; + const char *type; /* the ACME error string */ + apr_status_t rv; /* what Apache status code we give it */ + int input_related; /* if error indicates wrong input value */ }; 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 }, + { "acme:error:badCSR", APR_EINVAL, 1 }, + { "acme:error:badNonce", APR_EAGAIN, 0 }, + { "acme:error:badSignatureAlgorithm", APR_EINVAL, 1 }, + { "acme:error:externalAccountRequired", APR_EINVAL, 1 }, + { "acme:error:invalidContact", APR_BADARG, 1 }, + { "acme:error:unsupportedContact", APR_EGENERAL, 1 }, + { "acme:error:malformed", APR_EINVAL, 1 }, + { "acme:error:rateLimited", APR_BADARG, 0 }, + { "acme:error:rejectedIdentifier", APR_BADARG, 1 }, + { "acme:error:serverInternal", APR_EGENERAL, 0 }, + { "acme:error:unauthorized", APR_EACCES, 0 }, + { "acme:error:unsupportedIdentifier", APR_BADARG, 1 }, + { "acme:error:userActionRequired", APR_EAGAIN, 0 }, + { "acme:error:badRevocationReason", APR_EINVAL, 1 }, + { "acme:error:caa", APR_EGENERAL, 0 }, + { "acme:error:dns", APR_EGENERAL, 0 }, + { "acme:error:connection", APR_EGENERAL, 0 }, + { "acme:error:tls", APR_EGENERAL, 0 }, + { "acme:error:incorrectResponse", APR_EGENERAL, 0 }, }; static apr_status_t problem_status_get(const char *type) { @@ -85,89 +88,23 @@ static apr_status_t problem_status_get(const char *type) { return APR_EGENERAL; } -apr_status_t md_acme_init(apr_pool_t *p, const char *base) -{ - base_product = base; - return md_crypt_init(p); -} +int md_acme_problem_is_input_related(const char *problem) { + size_t i; -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; + if (!problem) return 0; + if (strstr(problem, "urn:ietf:params:") == problem) { + problem += strlen("urn:ietf:params:"); } - - 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; + else if (strstr(problem, "urn:") == problem) { + problem += strlen("urn:"); } - - 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; + for(i = 0; i < (sizeof(Problems)/sizeof(Problems[0])); ++i) { + if (!apr_strnatcasecmp(problem, Problems[i].type)) { + return Problems[i].input_related; } - 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; + return 0; } /**************************************************************************************************/ @@ -183,26 +120,10 @@ static void req_update_nonce(md_acme_t *acme, apr_table_t *hdrs) } } -static apr_status_t http_update_nonce(const md_http_response_t *res) +static apr_status_t http_update_nonce(const md_http_response_t *res, void *data) { - 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; + req_update_nonce(data, res->headers); + return APR_SUCCESS; } static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, const char *url) @@ -215,6 +136,7 @@ static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, co if (rv != APR_SUCCESS) { return NULL; } + apr_pool_tag(pool, "md_acme_req"); req = apr_pcalloc(pool, sizeof(*req)); if (!req) { @@ -226,54 +148,46 @@ static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, co 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->prot_fields = md_json_create(pool); req->max_retries = acme->max_retries; - + req->result = md_result_make(req->p, APR_SUCCESS); return req; } -apr_status_t md_acme_req_body_init(md_acme_req_t *req, md_json_t *jpayload) +static apr_status_t acmev2_new_nonce(md_acme_t *acme) { - 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; - } + return md_http_HEAD_perform(acme->http, acme->api.v2.new_nonce, NULL, http_update_nonce, acme); +} - 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); -} +apr_status_t md_acme_init(apr_pool_t *p, const char *base, int init_ssl) +{ + base_product = base; + return init_ssl? md_crypt_init(p) : APR_SUCCESS; +} static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t *res) { const char *ctype; - md_json_t *problem; - + md_json_t *problem = NULL; + apr_status_t rv; + ctype = apr_table_get(req->resp_hdrs, "content-type"); + ctype = md_util_parse_ct(res->req->pool, ctype); if (ctype && !strcmp(ctype, "application/problem+json")) { /* RFC 7807 */ - md_json_read_http(&problem, req->p, res); - if (problem) { + rv = md_json_read_http(&problem, req->p, res); + if (rv == APR_SUCCESS && 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); + md_result_problem_set(req->result, req->rv, ptype, pdetail, + md_json_getj(problem, MD_KEY_SUBPROBLEMS, NULL)); + + if (APR_STATUS_IS_EAGAIN(req->rv)) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, req->rv, req->p, @@ -287,43 +201,79 @@ static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t } } - 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; - } + switch (res->status) { + case 400: + return APR_EINVAL; + case 401: /* sectigo returns this instead of 403 */ + 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); + md_result_printf(req->result, APR_EGENERAL, "unexpected http status: %d", + res->status); + return req->result->status; } - return res->rv; + return APR_SUCCESS; } /**************************************************************************************************/ /* ACME requests with nonce handling */ -static apr_status_t md_acme_req_done(md_acme_req_t *req) +static apr_status_t acmev2_req_init(md_acme_req_t *req, md_json_t *jpayload) +{ + md_data_t payload; + + md_data_null(&payload); + if (!req->acme->acct) { + return APR_EINVAL; + } + if (jpayload) { + payload.data = md_json_writep(jpayload, req->p, MD_JSON_FMT_COMPACT); + if (!payload.data) { + return APR_EINVAL; + } + } + else { + payload.data = ""; + } + + payload.len = strlen(payload.data); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->p, + "acme payload(len=%" APR_SIZE_T_FMT "): %s", payload.len, payload.data); + return md_jws_sign(&req->req_json, req->p, &payload, + req->prot_fields, req->acme->acct_key, req->acme->acct->url); +} + +apr_status_t md_acme_req_body_init(md_acme_req_t *req, md_json_t *payload) { - apr_status_t rv = req->rv; + return req->acme->req_init_fn(req, payload); +} + +static apr_status_t md_acme_req_done(md_acme_req_t *req, apr_status_t rv) +{ + if (req->result->status != APR_SUCCESS) { + if (req->on_err) { + req->on_err(req, req->result, req->baton); + } + } + /* An error in rv superceeds the result->status */ + if (APR_SUCCESS != rv) req->result->status = rv; + rv = req->result->status; + /* transfer results into the acme's central result for longer life and later inspection */ + md_result_dup(req->acme->last, req->result); if (req->p) { apr_pool_destroy(req->p); } return rv; } -static apr_status_t on_response(const md_http_response_t *res) +static apr_status_t on_response(const md_http_response_t *res, void *data) { - md_acme_req_t *req = res->req->baton; - apr_status_t rv = res->rv; - - if (APR_SUCCESS != rv) { - goto out; - } + md_acme_req_t *req = data; + apr_status_t rv = APR_SUCCESS; req->resp_hdrs = apr_table_clone(req->p, res->headers); req_update_nonce(req->acme, res->headers); @@ -361,9 +311,10 @@ static apr_status_t on_response(const md_http_response_t *res) 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")); + md_result_printf(req->result, rv, "unable to process the response: " + "http-status=%d, content-type=%s", + res->status, apr_table_get(res->headers, "Content-Type")); + md_result_log(req->result, MD_LOG_ERR); } } else if (APR_EAGAIN == (rv = inspect_problem(req, res))) { @@ -371,85 +322,110 @@ static apr_status_t on_response(const md_http_response_t *res) return rv; } -out: - md_acme_req_done(req); + md_acme_req_done(req, rv); return rv; } +static apr_status_t acmev2_GET_as_POST_init(md_acme_req_t *req, void *baton) +{ + (void)baton; + return md_acme_req_body_init(req, NULL); +} + 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; + md_data_t *body = NULL; + md_result_t *result; assert(acme->url); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, req->p, + "sending req: %s %s", req->method, req->url); + md_result_reset(req->acme->last); + result = md_result_make(req->p, APR_SUCCESS); + + /* Whom are we talking to? */ + if (acme->version == MD_ACME_VERSION_UNKNOWN) { + rv = md_acme_setup(acme, result); + if (APR_SUCCESS != rv) goto leave; + } + + if (!strcmp("GET", req->method) && !req->on_init && !req->req_json) { + /* See <https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html#rfc.section.6.3> + * and <https://mailarchive.ietf.org/arch/msg/acme/sotffSQ0OWV-qQJodLwWYWcEVKI> + * and <https://community.letsencrypt.org/t/acme-v2-scheduled-deprecation-of-unauthenticated-resource-gets/74380> + * We implement this change in ACMEv2 and higher as keeping the md_acme_GET() methods, + * but switching them to POSTs with a empty, JWS signed, body when we call + * our HTTP client. */ + req->method = "POST"; + req->on_init = acmev2_GET_as_POST_init; + /*req->max_retries = 0; don't do retries on these "GET"s */ + } + + /* Besides GET/HEAD, we always need a fresh nonce */ 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->version == MD_ACME_VERSION_UNKNOWN) { + rv = md_acme_setup(acme, result); + if (APR_SUCCESS != rv) goto leave; } - 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; - } + if (!acme->nonce && (APR_SUCCESS != (rv = acme->new_nonce_fn(acme)))) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, req->p, + "error retrieving new nonce from ACME server"); + goto leave; } - - apr_table_set(req->prot_hdrs, "nonce", acme->nonce); + + md_json_sets(acme->nonce, req->prot_fields, "nonce", NULL); + md_json_sets(req->url, req->prot_fields, "url", NULL); acme->nonce = NULL; } rv = req->on_init? req->on_init(req, req->baton) : APR_SUCCESS; + if (APR_SUCCESS != rv) goto leave; - 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 (req->req_json) { + body = apr_pcalloc(req->p, sizeof(*body)); + body->data = md_json_writep(req->req_json, req->p, MD_JSON_FMT_INDENT); + body->len = strlen(body->data); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->p, + "sending JSON body: %s", body->data); } - 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 (body && md_log_is_level(req->p, MD_LOG_TRACE4)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->p, + "req: %s %s, body:\n%s", req->method, req->url, body->data); } - - if (req) { - md_acme_req_done(req); + else { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, req->p, + "req: %s %s", req->method, req->url); + } + + if (!strcmp("GET", req->method)) { + rv = md_http_GET_perform(req->acme->http, req->url, NULL, on_response, req); + } + else if (!strcmp("POST", req->method)) { + rv = md_http_POSTd_perform(req->acme->http, req->url, NULL, "application/jose+json", + body, on_response, req); + } + else if (!strcmp("HEAD", req->method)) { + rv = md_http_HEAD_perform(req->acme->http, req->url, NULL, on_response, req); + } + 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"); + + if (APR_EAGAIN == rv && req->max_retries > 0) { + --req->max_retries; + rv = md_acme_req_send(req); } + req = NULL; + +leave: + if (req) md_acme_req_done(req, rv); return rv; } @@ -457,6 +433,7 @@ 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, + md_acme_req_err_cb *on_err, void *baton) { md_acme_req_t *req; @@ -469,6 +446,7 @@ apr_status_t md_acme_POST(md_acme_t *acme, const char *url, req->on_init = on_init; req->on_json = on_json; req->on_res = on_res; + req->on_err = on_err; req->baton = baton; return md_acme_req_send(req); @@ -478,6 +456,7 @@ 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, + md_acme_req_err_cb *on_err, void *baton) { md_acme_req_t *req; @@ -490,11 +469,23 @@ apr_status_t md_acme_GET(md_acme_t *acme, const char *url, req->on_init = on_init; req->on_json = on_json; req->on_res = on_res; + req->on_err = on_err; req->baton = baton; return md_acme_req_send(req); } +void md_acme_report_result(md_acme_t *acme, apr_status_t rv, struct md_result_t *result) +{ + if (acme->last->status == APR_SUCCESS) { + md_result_set(result, rv, NULL); + } + else { + md_result_problem_set(result, acme->last->status, acme->last->problem, + acme->last->detail, acme->last->subproblems); + } +} + /**************************************************************************************************/ /* GET JSON */ @@ -524,8 +515,283 @@ apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme, ctx.pool = p; ctx.json = NULL; - rv = md_acme_GET(acme, url, NULL, on_got_json, NULL, &ctx); + rv = md_acme_GET(acme, url, NULL, on_got_json, NULL, NULL, &ctx); *pjson = (APR_SUCCESS == rv)? ctx.json : NULL; return rv; } +/**************************************************************************************************/ +/* Generic ACME operations */ + +void md_acme_clear_acct(md_acme_t *acme) +{ + acme->acct_id = NULL; + acme->acct = NULL; + acme->acct_key = NULL; +} + +const char *md_acme_acct_id_get(md_acme_t *acme) +{ + return acme->acct_id; +} + +const char *md_acme_acct_url_get(md_acme_t *acme) +{ + return acme->acct? acme->acct->url : NULL; +} + +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 (md_acme_acct_matches_url(acct, acme->url)) { + acme->acct_id = apr_pstrdup(p, acct_id); + acme->acct = acct; + acme->acct_key = pkey; + rv = md_acme_acct_validate(acme, store, p); + } + else { + /* account is from another server or, more likely, from another + * protocol endpoint on the same server */ + rv = APR_ENOENT; + } + } + return rv; +} + +apr_status_t md_acme_use_acct_for_md(md_acme_t *acme, struct md_store_t *store, + apr_pool_t *p, const char *acct_id, + const md_t *md) +{ + 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 (md_acme_acct_matches_md(acct, md)) { + acme->acct_id = apr_pstrdup(p, acct_id); + acme->acct = acct; + acme->acct_key = pkey; + rv = md_acme_acct_validate(acme, store, p); + } + else { + /* account is from another server or, more likely, from another + * protocol endpoint on the same server */ + rv = APR_ENOENT; + } + } + return rv; +} + +apr_status_t md_acme_save_acct(md_acme_t *acme, apr_pool_t *p, md_store_t *store) +{ + return md_acme_acct_save(store, p, acme, &acme->acct_id, acme->acct, acme->acct_key); +} + +static apr_status_t acmev2_POST_new_account(md_acme_t *acme, + md_acme_req_init_cb *on_init, + md_acme_req_json_cb *on_json, + md_acme_req_res_cb *on_res, + md_acme_req_err_cb *on_err, + void *baton) +{ + return md_acme_POST(acme, acme->api.v2.new_account, on_init, on_json, on_res, on_err, baton); +} + +apr_status_t md_acme_POST_new_account(md_acme_t *acme, + md_acme_req_init_cb *on_init, + md_acme_req_json_cb *on_json, + md_acme_req_res_cb *on_res, + md_acme_req_err_cb *on_err, + void *baton) +{ + return acme->post_new_account_fn(acme, on_init, on_json, on_res, on_err, baton); +} + +/**************************************************************************************************/ +/* ACME setup */ + +apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url, + const char *proxy_url, const char *ca_file) +{ + 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 = 99; + acme->ca_file = ca_file; + + 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); + acme->version = MD_ACME_VERSION_UNKNOWN; + acme->last = md_result_make(acme->p, APR_SUCCESS); + + *pacme = acme; + return rv; +} + +typedef struct { + md_acme_t *acme; + md_result_t *result; +} update_dir_ctx; + +static apr_status_t update_directory(const md_http_response_t *res, void *data) +{ + md_http_request_t *req = res->req; + md_acme_t *acme = ((update_dir_ctx *)data)->acme; + md_result_t *result = ((update_dir_ctx *)data)->result; + apr_status_t rv; + md_json_t *json; + const char *s; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->pool, "directory lookup response: %d", res->status); + if (res->status == 503) { + md_result_printf(result, APR_EAGAIN, + "The ACME server at <%s> reports that Service is Unavailable (503). This " + "may happen during maintenance for short periods of time.", acme->url); + md_result_log(result, MD_LOG_INFO); + rv = result->status; + goto leave; + } + else if (res->status < 200 || res->status >= 300) { + md_result_printf(result, APR_EAGAIN, + "The ACME server at <%s> responded with HTTP status %d. This " + "is unusual. Please verify that the URL is correct and that you can indeed " + "make request from the server to it by other means, e.g. invoking curl/wget.", + acme->url, res->status); + rv = result->status; + goto leave; + } + + rv = md_json_read_http(&json, req->pool, res); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, req->pool, "reading JSON body"); + goto leave; + } + + if (md_log_is_level(acme->p, MD_LOG_TRACE2)) { + s = md_json_writep(json, req->pool, MD_JSON_FMT_INDENT); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, req->pool, + "response: %s", s ? s : "<failed to serialize!>"); + } + + /* What have we got? */ + if ((s = md_json_dups(acme->p, json, "newAccount", NULL))) { + acme->api.v2.new_account = s; + acme->api.v2.new_order = md_json_dups(acme->p, json, "newOrder", NULL); + acme->api.v2.revoke_cert = md_json_dups(acme->p, json, "revokeCert", NULL); + acme->api.v2.key_change = md_json_dups(acme->p, json, "keyChange", NULL); + acme->api.v2.new_nonce = md_json_dups(acme->p, json, "newNonce", NULL); + /* RFC 8555 only requires "directory" and "newNonce" resources. + * mod_md uses "newAccount" and "newOrder" so check for them. + * But mod_md does not use the "revokeCert" or "keyChange" + * resources, so tolerate the absence of those keys. In the + * future if mod_md implements revocation or key rollover then + * the use of those features should be predicated on the + * server's advertised capabilities. */ + if (acme->api.v2.new_account + && acme->api.v2.new_order + && acme->api.v2.new_nonce) { + acme->version = MD_ACME_VERSION_2; + } + acme->ca_agreement = md_json_dups(acme->p, json, "meta", MD_KEY_TOS, NULL); + acme->eab_required = md_json_getb(json, "meta", MD_KEY_EAB_REQUIRED, NULL); + acme->new_nonce_fn = acmev2_new_nonce; + acme->req_init_fn = acmev2_req_init; + acme->post_new_account_fn = acmev2_POST_new_account; + } + else if ((s = md_json_dups(acme->p, json, "new-authz", NULL))) { + acme->api.v1.new_authz = s; + acme->api.v1.new_cert = md_json_dups(acme->p, json, "new-cert", NULL); + acme->api.v1.new_reg = md_json_dups(acme->p, json, "new-reg", NULL); + acme->api.v1.revoke_cert = md_json_dups(acme->p, json, "revoke-cert", NULL); + if (acme->api.v1.new_authz && acme->api.v1.new_cert + && acme->api.v1.new_reg && acme->api.v1.revoke_cert) { + acme->version = MD_ACME_VERSION_1; + } + acme->ca_agreement = md_json_dups(acme->p, json, "meta", "terms-of-service", NULL); + /* we init that far, but will not use the v1 api */ + } + + if (MD_ACME_VERSION_UNKNOWN == acme->version) { + md_result_printf(result, APR_EINVAL, + "Unable to understand ACME server response from <%s>. " + "Wrong ACME protocol version or link?", acme->url); + md_result_log(result, MD_LOG_WARNING); + rv = result->status; + } +leave: + return rv; +} + +apr_status_t md_acme_setup(md_acme_t *acme, md_result_t *result) +{ + apr_status_t rv; + update_dir_ctx ctx; + + assert(acme->url); + acme->version = MD_ACME_VERSION_UNKNOWN; + + if (!acme->http && APR_SUCCESS != (rv = md_http_create(&acme->http, acme->p, + acme->user_agent, acme->proxy_url))) { + return rv; + } + /* TODO: maybe this should be configurable. Let's take some reasonable + * defaults for now that protect our client */ + md_http_set_response_limit(acme->http, 1024*1024); + md_http_set_timeout_default(acme->http, apr_time_from_sec(10 * 60)); + md_http_set_connect_timeout_default(acme->http, apr_time_from_sec(30)); + md_http_set_stalling_default(acme->http, 10, apr_time_from_sec(30)); + md_http_set_ca_file(acme->http, acme->ca_file); + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "get directory from %s", acme->url); + + ctx.acme = acme; + ctx.result = result; + rv = md_http_GET_perform(acme->http, acme->url, NULL, update_directory, &ctx); + + if (APR_SUCCESS != rv && APR_SUCCESS == result->status) { + /* If the result reports no error, we never got a response from the server */ + md_result_printf(result, rv, + "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. Apache will " + "continue retrying this.", acme->url); + md_result_log(result, MD_LOG_WARNING); + } + return rv; +} + + diff --git a/modules/md/md_acme.h b/modules/md/md_acme.h index 2dcbee6..f28f2b6 100644 --- a/modules/md/md_acme.h +++ b/modules/md/md_acme.h @@ -26,14 +26,21 @@ struct md_json_t; struct md_pkey_t; struct md_t; struct md_acme_acct_t; -struct md_proto_t; +struct md_acmev2_acct_t; struct md_store_t; +struct md_result_t; #define MD_PROTO_ACME "ACME" #define MD_AUTHZ_CHA_HTTP_01 "http-01" #define MD_AUTHZ_CHA_SNI_01 "tls-sni-01" +#define MD_ACME_VERSION_UNKNOWN 0x0 +#define MD_ACME_VERSION_1 0x010000 +#define MD_ACME_VERSION_2 0x020000 + +#define MD_ACME_VERSION_MAJOR(i) (((i)&0xFF0000) >> 16) + typedef enum { MD_ACME_S_UNKNOWN, /* MD has not been analysed yet */ MD_ACME_S_REGISTERED, /* MD is registered at CA, but not more */ @@ -46,30 +53,92 @@ typedef enum { typedef struct md_acme_t md_acme_t; +typedef struct md_acme_req_t md_acme_req_t; +/** + * 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); + +/** + * 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); + +/** + * Request callback on detected errors. + */ +typedef apr_status_t md_acme_req_err_cb(md_acme_req_t *req, + const struct md_result_t *result, void *baton); + + +typedef apr_status_t md_acme_new_nonce_fn(md_acme_t *acme); +typedef apr_status_t md_acme_req_init_fn(md_acme_req_t *req, struct md_json_t *jpayload); + +typedef apr_status_t md_acme_post_fn(md_acme_t *acme, + md_acme_req_init_cb *on_init, + md_acme_req_json_cb *on_json, + md_acme_req_res_cb *on_res, + md_acme_req_err_cb *on_err, + void *baton); + 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 *ca_file; - const char *new_authz; - const char *new_cert; - const char *new_reg; - const char *revoke_cert; + const char *acct_id; /* local storage id account was loaded from or NULL */ + struct md_acme_acct_t *acct; /* account at ACME server to use for requests */ + struct md_pkey_t *acct_key; /* private RSA key belonging to account */ + + int version; /* as detected from the server */ + union { + struct { /* obsolete */ + const char *new_authz; + const char *new_cert; + const char *new_reg; + const char *revoke_cert; + + } v1; + struct { + const char *new_account; + const char *new_order; + const char *key_change; + const char *revoke_cert; + const char *new_nonce; + } v2; + } api; + const char *ca_agreement; + const char *acct_name; + int eab_required; + + md_acme_new_nonce_fn *new_nonce_fn; + md_acme_req_init_fn *req_init_fn; + md_acme_post_fn *post_new_account_fn; struct md_http_t *http; const char *nonce; int max_retries; + struct md_result_t *last; /* result of last request */ }; /** * Global init, call once at start up. */ -apr_status_t md_acme_init(apr_pool_t *pool, const char *base_version); +apr_status_t md_acme_init(apr_pool_t *pool, const char *base_version, int init_ssl); /** * Create a new ACME server instance. If path is not NULL, will use that directory @@ -82,39 +151,68 @@ apr_status_t md_acme_init(apr_pool_t *pool, const char *base_version); * @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); + const char *proxy_url, const char *ca_file); /** * 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); +apr_status_t md_acme_setup(md_acme_t *acme, struct md_result_t *result); + +void md_acme_report_result(md_acme_t *acme, apr_status_t rv, struct md_result_t *result); /**************************************************************************************************/ /* account handling */ -#define MD_ACME_ACCT_STAGED "staged" +/** + * Clear any existing account data from acme instance. + */ +void md_acme_clear_acct(md_acme_t *acme); + +apr_status_t md_acme_POST_new_account(md_acme_t *acme, + md_acme_req_init_cb *on_init, + md_acme_req_json_cb *on_json, + md_acme_req_res_cb *on_res, + md_acme_req_err_cb *on_err, + void *baton); -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); +/** + * 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_acct_id_get(md_acme_t *acme); +const char *md_acme_acct_url_get(md_acme_t *acme); /** * Specify the account to use by name in local store. On success, the account - * the "current" one used by the acme instance. + * is the "current" one used by the acme instance. + * @param acme the acme instance to set the account for + * @param store the store to load accounts from + * @param p pool for allocations + * @param acct_id name of the account to load */ 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); +/** + * Specify the account to use for a specific MD by name in local store. + * On success, the account is the "current" one used by the acme instance. + * @param acme the acme instance to set the account for + * @param store the store to load accounts from + * @param p pool for allocations + * @param acct_id name of the account to load + * @param md the MD the account shall be used for + */ +apr_status_t md_acme_use_acct_for_md(md_acme_t *acme, struct md_store_t *store, + apr_pool_t *p, const char *acct_id, + const md_t *md); /** * 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); +const char *md_acme_acct_id_get(md_acme_t *acme); /** * Agree to the given Terms-of-Service url for the current account. @@ -136,78 +234,23 @@ apr_status_t md_acme_agree(md_acme_t *acme, apr_pool_t *p, const char *tos); 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_acct(md_acme_t *acme, apr_pool_t *p, struct md_store_t *store); -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. + * Deactivate the current account at the ACME server.. */ -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); +apr_status_t md_acme_acct_deactivate(md_acme_t *acme, apr_pool_t *p); /**************************************************************************************************/ /* 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 *prot_fields; /* JWS protected fields */ struct md_json_t *req_json; /* JSON to be POSTed in request body */ apr_table_t *resp_hdrs; /* HTTP response headers */ @@ -218,14 +261,19 @@ struct md_acme_req_t { 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 */ + md_acme_req_err_cb *on_err; /* callback on encountered error */ int max_retries; /* how often this might be retried */ void *baton; /* userdata for callbacks */ + struct md_result_t *result; /* result of this request */ }; +apr_status_t md_acme_req_body_init(md_acme_req_t *req, struct md_json_t *payload); + 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, + md_acme_req_err_cb *on_err, void *baton); /** * Perform a POST against the ACME url. If a on_json callback is given and @@ -245,14 +293,9 @@ 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, + md_acme_req_err_cb *on_err, 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 */ @@ -264,4 +307,11 @@ apr_status_t md_acme_req_body_init(md_acme_req_t *req, struct md_json_t *jpayloa apr_status_t md_acme_protos_add(struct apr_hash_t *protos, apr_pool_t *p); +/** + * Return != 0 iff the given problem identifier is an ACME error string + * indicating something is wrong with the input values, e.g. from our + * configuration. + */ +int md_acme_problem_is_input_related(const char *problem); + #endif /* md_acme_h */ diff --git a/modules/md/md_acme_acct.c b/modules/md/md_acme_acct.c index c4a2b5f..f3e043e 100644 --- a/modules/md/md_acme_acct.c +++ b/modules/md/md_acme_acct.c @@ -30,6 +30,7 @@ #include "md_json.h" #include "md_jws.h" #include "md_log.h" +#include "md_result.h" #include "md_store.h" #include "md_util.h" #include "md_version.h" @@ -38,15 +39,12 @@ #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) + const char *ca_url, 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 *)); } @@ -72,87 +70,118 @@ static const char *mk_acct_pattern(apr_pool_t *p, md_acme_t *acme) /**************************************************************************************************/ /* json load/save */ -static md_json_t *acct_to_json(md_acme_acct_t *acct, apr_pool_t *p) +static md_acme_acct_st acct_st_from_str(const char *s) +{ + if (s) { + if (!strcmp("valid", s)) { + return MD_ACME_ACCT_ST_VALID; + } + else if (!strcmp("deactivated", s)) { + return MD_ACME_ACCT_ST_DEACTIVATED; + } + else if (!strcmp("revoked", s)) { + return MD_ACME_ACCT_ST_REVOKED; + } + } + return MD_ACME_ACCT_ST_UNKNOWN; +} + +md_json_t *md_acme_acct_to_json(md_acme_acct_t *acct, apr_pool_t *p) { md_json_t *jacct; + const char *s; 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); - } - + switch (acct->status) { + case MD_ACME_ACCT_ST_VALID: + s = "valid"; + break; + case MD_ACME_ACCT_ST_DEACTIVATED: + s = "deactivated"; + break; + case MD_ACME_ACCT_ST_REVOKED: + s = "revoked"; + break; + default: + s = NULL; + break; + } + if (s) md_json_sets(s, jacct, MD_KEY_STATUS, NULL); + if (acct->url) md_json_sets(acct->url, jacct, MD_KEY_URL, NULL); + if (acct->ca_url) md_json_sets(acct->ca_url, jacct, MD_KEY_CA_URL, NULL); + if (acct->contacts) md_json_setsa(acct->contacts, jacct, MD_KEY_CONTACT, NULL); + if (acct->registration) md_json_setj(acct->registration, jacct, MD_KEY_REGISTRATION, NULL); + if (acct->agreement) md_json_sets(acct->agreement, jacct, MD_KEY_AGREEMENT, NULL); + if (acct->orders) md_json_sets(acct->orders, jacct, MD_KEY_ORDERS, NULL); + if (acct->eab_kid) md_json_sets(acct->eab_kid, jacct, MD_KEY_EAB, MD_KEY_KID, NULL); + if (acct->eab_hmac) md_json_sets(acct->eab_hmac, jacct, MD_KEY_EAB, MD_KEY_HMAC, 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 md_acme_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; + md_acme_acct_st status = MD_ACME_ACCT_ST_UNKNOWN; + const char *ca_url, *url; 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; + if (md_json_has_key(json, MD_KEY_STATUS, NULL)) { + status = acct_st_from_str(md_json_gets(json, MD_KEY_STATUS, NULL)); } - + 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; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no url"); + goto leave; } + 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", url); + goto leave; + } + 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; + if (md_json_has_key(json, MD_KEY_CONTACT, NULL)) { + md_json_getsa(contacts, json, MD_KEY_CONTACT, NULL); + } + else { + md_json_getsa(contacts, json, MD_KEY_REGISTRATION, MD_KEY_CONTACT, NULL); + } + rv = acct_make(&acct, p, ca_url, contacts); + if (APR_SUCCESS != rv) goto leave; + + acct->status = status; + acct->url = url; + acct->agreement = md_json_gets(json, MD_KEY_AGREEMENT, NULL); + if (!acct->agreement) { + /* backward compatible check */ acct->agreement = md_json_gets(json, "terms-of-service", NULL); } + acct->orders = md_json_gets(json, MD_KEY_ORDERS, NULL); + if (md_json_has_key(json, MD_KEY_EAB, MD_KEY_KID, NULL) + && md_json_has_key(json, MD_KEY_EAB, MD_KEY_HMAC, NULL)) { + acct->eab_kid = md_json_gets(json, MD_KEY_EAB, MD_KEY_KID, NULL); + acct->eab_hmac = md_json_gets(json, MD_KEY_EAB, MD_KEY_HMAC, NULL); + } -out: +leave: *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) + const char **pid, 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; + const char *id = pid? *pid : NULL; + jacct = md_acme_acct_to_json(acct, p); if (id) { rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 0); } @@ -160,23 +189,16 @@ apr_status_t md_acme_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme 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; + if (pid) *pid = 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) @@ -193,11 +215,11 @@ apr_status_t md_acme_acct_load(md_acme_acct_t **pacct, md_pkey_t **ppkey, goto out; } - rv = acct_from_json(pacct, json, p); + rv = md_acme_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); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "loading key: %s", name); goto out; } } @@ -212,9 +234,36 @@ out: /**************************************************************************************************/ /* Lookup */ +int md_acme_acct_matches_url(md_acme_acct_t *acct, const char *url) +{ + /* The ACME url must match exactly */ + if (!url || !acct->ca_url || strcmp(acct->ca_url, url)) return 0; + return 1; +} + +int md_acme_acct_matches_md(md_acme_acct_t *acct, const md_t *md) +{ + if (!md_acme_acct_matches_url(acct, md->ca_effective)) return 0; + /* if eab values are not mentioned, we match an account regardless + * if it was registered with eab or not */ + if (!md->ca_eab_kid || !md->ca_eab_hmac) { + /* No eab only acceptable when no eab is asked for. + * This prevents someone that has no external account binding + * to re-use an account from another MDomain that was created + * with a binding. */ + return !acct->eab_kid || !acct->eab_hmac; + } + /* But of eab is asked for, we need an acct that matches exactly. + * When someone configures a new EAB and we need + * to created a new account for it. */ + if (!acct->eab_kid || !acct->eab_hmac) return 0; + return !strcmp(acct->eab_kid, md->ca_eab_kid) + && !strcmp(acct->eab_hmac, md->ca_eab_hmac); +} + typedef struct { apr_pool_t *p; - md_acme_t *acme; + const md_t *md; const char *id; } find_ctx; @@ -222,232 +271,227 @@ 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; - + md_acme_acct_t *acct; + apr_status_t rv; + (void)aspect; (void)ptemp; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, "account candidate %s/%s", name, aspect); 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)) { + rv = md_acme_acct_from_json(&acct, (md_json_t*)value, ptemp); + if (APR_SUCCESS != rv) goto cleanup; + + if (MD_ACME_ACCT_ST_VALID == acct->status + && (!ctx->md || md_acme_acct_matches_md(acct, ctx->md))) { 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; + "found account %s for %s: %s, status=%d", + acct->id, ctx->md->ca_effective, aspect, acct->status); + ctx->id = apr_pstrdup(ctx->p, name); return 0; } } +cleanup: 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) +static apr_status_t acct_find(const char **pid, md_acme_acct_t **pacct, md_pkey_t **ppkey, + md_store_t *store, md_store_group_t group, + const char *name_pattern, + const md_t *md, apr_pool_t *p) { apr_status_t rv; find_ctx ctx; - + + memset(&ctx, 0, sizeof(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); + ctx.md = md; + + rv = md_store_iter(find_acct, &ctx, store, p, group, name_pattern, MD_FN_ACCOUNT, MD_SV_JSON); if (ctx.id) { - rv = md_acme_acct_load(pacct, ppkey, store, MD_SG_ACCOUNTS, ctx.id, p); + *pid = ctx.id; + rv = md_acme_acct_load(pacct, ppkey, store, group, ctx.id, p); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "acct_find: got account %s", ctx.id); } else { *pacct = NULL; rv = APR_ENOENT; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "acct_find: none found"); } - 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) +static apr_status_t acct_find_and_verify(md_store_t *store, md_store_group_t group, + const char *name_pattern, + md_acme_t *acme, const md_t *md, + apr_pool_t *p) { - acct_ctx_t *ctx = baton; - md_json_t *jpayload; + md_acme_acct_t *acct; + md_pkey_t *pkey; + const char *id; + apr_status_t rv; - 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); -} + rv = acct_find(&id, &acct, &pkey, store, group, name_pattern, md, p); + if (APR_SUCCESS == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "acct_find_and_verify: found %s", + id); + acme->acct_id = (MD_SG_STAGING == group)? NULL : id; + acme->acct = acct; + acme->acct_key = pkey; + rv = md_acme_acct_validate(acme, (MD_SG_STAGING == group)? NULL : store, p); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p, "acct_find_and_verify: verified %s", + id); -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); + if (APR_SUCCESS != rv) { + acme->acct_id = NULL; + acme->acct = NULL; + acme->acct_key = NULL; + if (APR_STATUS_IS_ENOENT(rv)) { + /* verification failed and account has been disabled. + Indicate to caller that he may try again. */ + rv = APR_EAGAIN; + } } } - - 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 md_acme_find_acct_for_md(md_acme_t *acme, md_store_t *store, const md_t *md) { 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; - } + while (APR_EAGAIN == (rv = acct_find_and_verify(store, MD_SG_ACCOUNTS, + mk_acct_pattern(acme->p, acme), + acme, md, acme->p))) { + /* nop */ } - 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; + + if (APR_STATUS_IS_ENOENT(rv)) { + /* No suitable account found in MD_SG_ACCOUNTS. Maybe a new account + * can already be found in MD_SG_STAGING? */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, + "no account found, looking in STAGING"); + rv = acct_find_and_verify(store, MD_SG_STAGING, "*", acme, md, acme->p); + if (APR_EAGAIN == rv) { + rv = APR_ENOENT; } } - - 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; + return rv; +} - acme->acct_key = pkey; - if (agreement) { - acme->acct->agreement = agreement; - } +apr_status_t md_acme_acct_id_for_md(const char **pid, md_store_t *store, + md_store_group_t group, const md_t *md, + apr_pool_t *p) +{ + apr_status_t rv; + find_ctx ctx; - 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); - } - } + memset(&ctx, 0, sizeof(ctx)); + ctx.p = p; + ctx.md = md; -out: - if (APR_SUCCESS != rv && acme->acct) { - acme->acct = NULL; + rv = md_store_iter(find_acct, &ctx, store, p, group, "*", MD_FN_ACCOUNT, MD_SV_JSON); + if (ctx.id) { + *pid = ctx.id; + rv = APR_SUCCESS; } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "acct_id_for_md %s -> %s", md->name, *pid); return rv; } /**************************************************************************************************/ -/* acct validation */ +/* acct operation context */ +typedef struct { + md_acme_t *acme; + apr_pool_t *p; + const char *agreement; + const char *eab_kid; + const char *eab_hmac; +} acct_ctx_t; -static apr_status_t on_init_acct_valid(md_acme_req_t *req, void *baton) -{ - md_json_t *jpayload; +/**************************************************************************************************/ +/* acct update */ +static apr_status_t on_init_acct_upd(md_acme_req_t *req, void *baton) +{ (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); + return md_acme_req_body_init(req, NULL); } -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) +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) { - md_acme_acct_t *acct = acme->acct; + acct_ctx_t *ctx = baton; apr_status_t rv = APR_SUCCESS; - const char *body_str; - const char *tos_required; + md_acme_acct_t *acct = acme->acct; + + if (md_log_is_level(p, MD_LOG_TRACE2)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, acme->p, "acct update response: %s", + md_json_writep(body, p, MD_JSON_FMT_COMPACT)); + } + + 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); + } - (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; + md_json_dupsa(acct->contacts, acme->p, body, MD_KEY_CONTACT, NULL); + if (md_json_has_key(body, MD_KEY_STATUS, NULL)) { + acct->status = acct_st_from_str(md_json_gets(body, MD_KEY_STATUS, NULL)); + } + if (md_json_has_key(body, MD_KEY_AGREEMENT, NULL)) { + acct->agreement = md_json_dups(acme->p, body, MD_KEY_AGREEMENT, NULL); } + if (md_json_has_key(body, MD_KEY_ORDERS, NULL)) { + acct->orders = md_json_dups(acme->p, body, MD_KEY_ORDERS, NULL); + } + if (ctx->eab_kid && ctx->eab_hmac) { + acct->eab_kid = ctx->eab_kid; + acct->eab_hmac = ctx->eab_hmac; + } + 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 md_acme_validate_acct(md_acme_t *acme) +apr_status_t md_acme_acct_update(md_acme_t *acme) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "acct validation"); + acct_ctx_t ctx; + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "acct update"); if (!acme->acct) { return APR_EINVAL; } - return md_acme_POST(acme, acme->acct->url, on_init_acct_valid, acct_valid, NULL, NULL); + memset(&ctx, 0, sizeof(ctx)); + ctx.acme = acme; + ctx.p = acme->p; + return md_acme_POST(acme, acme->acct->url, on_init_acct_upd, acct_upd, NULL, NULL, &ctx); } -/**************************************************************************************************/ -/* account setup */ - -static apr_status_t acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t *p) +apr_status_t md_acme_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 (APR_SUCCESS != (rv = md_acme_acct_update(acme))) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, acme->p, + "acct update failed for %s", acme->acct->url); + if (APR_EINVAL == rv && (acme->acct->agreement || !acme->ca_agreement)) { + /* Sadly, some proprietary ACME servers choke on empty POSTs + * on accounts. Try a faked ToS agreement. */ + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, acme->p, + "trying acct update via ToS agreement"); + rv = md_acme_agree(acme, p, "accepted"); + } + if (acme->acct && (APR_ENOENT == rv || APR_EACCES == rv || APR_EINVAL == rv)) { + if (MD_ACME_ACCT_ST_VALID == acme->acct->status) { + acme->acct->status = MD_ACME_ACCT_ST_UNKNOWN; if (store) { - md_acme_save(acme, store, p); + md_acme_acct_save(store, p, acme, &acme->acct_id, acme->acct, acme->acct_key); } } acme->acct = NULL; @@ -458,133 +502,187 @@ static apr_status_t acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t 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) +/**************************************************************************************************/ +/* Register a new account */ + +static apr_status_t get_eab(md_json_t **peab, md_acme_req_t *req, const char *kid, + const char *hmac64, md_pkey_t *account_key, + const char *url) { - md_acme_acct_t *acct; - md_pkey_t *pkey; + md_json_t *eab, *prot_fields, *jwk; + md_data_t payload, hmac_key; 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; - } + + prot_fields = md_json_create(req->p); + md_json_sets(url, prot_fields, "url", NULL); + md_json_sets(kid, prot_fields, "kid", NULL); + + rv = md_jws_get_jwk(&jwk, req->p, account_key); + if (APR_SUCCESS != rv) goto cleanup; + + md_data_null(&payload); + payload.data = md_json_writep(jwk, req->p, MD_JSON_FMT_COMPACT); + if (!payload.data) { + rv = APR_EINVAL; + goto cleanup; } - return rv; -} + payload.len = strlen(payload.data); -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); + md_util_base64url_decode(&hmac_key, hmac64, req->p); + if (!hmac_key.len) { + rv = APR_EINVAL; + md_result_problem_set(req->result, rv, "apache:eab-hmac-invalid", + "external account binding HMAC value is not valid base64", NULL); + goto cleanup; } + + rv = md_jws_hmac(&eab, req->p, &payload, prot_fields, &hmac_key); + if (APR_SUCCESS != rv) { + md_result_problem_set(req->result, rv, "apache:eab-hmac-fail", + "external account binding MAC could not be computed", NULL); + } + +cleanup: + *peab = (APR_SUCCESS == rv)? eab : NULL; return rv; } -const char *md_acme_get_acct_id(md_acme_t *acme) +static apr_status_t on_init_acct_new(md_acme_req_t *req, void *baton) { - return acme->acct? acme->acct->id : NULL; -} + acct_ctx_t *ctx = baton; + md_json_t *jpayload, *jeab; + apr_status_t rv; -const char *md_acme_get_agreement(md_acme_t *acme) -{ - return acme->acct? acme->acct->agreement : NULL; -} + jpayload = md_json_create(req->p); + md_json_setsa(ctx->acme->acct->contacts, jpayload, MD_KEY_CONTACT, NULL); + if (ctx->agreement) { + md_json_setb(1, jpayload, "termsOfServiceAgreed", NULL); + } + if (ctx->eab_kid && ctx->eab_hmac) { + rv = get_eab(&jeab, req, ctx->eab_kid, ctx->eab_hmac, + req->acme->acct_key, req->url); + if (APR_SUCCESS != rv) goto cleanup; + md_json_setj(jeab, jpayload, "externalAccountBinding", NULL); + } + rv = md_acme_req_body_init(req, jpayload); + +cleanup: + return rv; +} -apr_status_t md_acme_find_acct(md_acme_t *acme, md_store_t *store, apr_pool_t *p) +apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store, + const md_t *md, apr_pool_t *p) { - md_acme_acct_t *acct; - md_pkey_t *pkey; apr_status_t rv; + md_pkey_t *pkey; + const char *err = NULL, *uri; + md_pkey_spec_t spec; + int i; + acct_ctx_t ctx; - 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; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "create new account"); + + memset(&ctx, 0, sizeof(ctx)); + ctx.acme = acme; + ctx.p = p; + /* The agreement URL is submitted when the ACME server announces Terms-of-Service + * in its directory meta data. The magic value "accepted" will always use the + * advertised URL. */ + ctx.agreement = NULL; + if (acme->ca_agreement && md->ca_agreement) { + ctx.agreement = !strcmp("accepted", md->ca_agreement)? + acme->ca_agreement : md->ca_agreement; + } + + if (ctx.agreement) { + if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, ctx.agreement, &err))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "invalid agreement uri (%s): %s", err, ctx.agreement); + goto out; } - else { - acme->acct = NULL; - acme->acct_key = NULL; - if (!APR_STATUS_IS_ENOENT(rv)) { - /* encountered error with server */ - return rv; + } + ctx.eab_kid = md->ca_eab_kid; + ctx.eab_hmac = md->ca_eab_hmac; + + for (i = 0; i < md->contacts->nelts; ++i) { + uri = APR_ARRAY_IDX(md->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; + } + } + + /* If there is no key selected yet, try to find an existing one for the same host. + * Let's Encrypt identifies accounts by their key for their ACMEv1 and v2 services. + * Although the account appears on both services with different urls, it is + * internally the same one. + * I think this is beneficial if someone migrates from ACMEv1 to v2 and not a leak + * of identifying information. + */ + if (!acme->acct_key) { + find_ctx fctx; + + memset(&fctx, 0, sizeof(fctx)); + fctx.p = p; + fctx.md = md; + + md_store_iter(find_acct, &fctx, store, p, MD_SG_ACCOUNTS, + mk_acct_pattern(p, acme), MD_FN_ACCOUNT, MD_SV_JSON); + if (fctx.id) { + rv = md_store_load(store, MD_SG_ACCOUNTS, fctx.id, MD_FN_ACCT_KEY, MD_SV_PKEY, + (void**)&acme->acct_key, p); + if (APR_SUCCESS == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "reusing key from account %s", fctx.id); + } + else { + acme->acct_key = NULL; } } } - 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 we still have no key, generate a new one */ + if (!acme->acct_key) { + 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))) goto out; + acme->acct_key = pkey; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "created new account key"); + } + + if (APR_SUCCESS != (rv = acct_make(&acme->acct, p, acme->url, md->contacts))) goto out; + rv = md_acme_POST_new_account(acme, on_init_acct_new, acct_upd, NULL, NULL, &ctx); if (APR_SUCCESS == rv) { - md_store_remove(store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCT_KEY, p, 1); + 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; } +/**************************************************************************************************/ +/* Deactivate the account */ + 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); - + md_json_sets("deactivated", jpayload, MD_KEY_STATUS, 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) +apr_status_t md_acme_acct_deactivate(md_acme_t *acme, apr_pool_t *p) { md_acme_acct_t *acct = acme->acct; + acct_ctx_t ctx; (void)p; if (!acct) { @@ -592,7 +690,10 @@ apr_status_t md_acme_delete_acct(md_acme_t *acme, md_store_t *store, apr_pool_t } 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); + memset(&ctx, 0, sizeof(ctx)); + ctx.acme = acme; + ctx.p = p; + return md_acme_POST(acme, acct->url, on_init_acct_del, acct_upd, NULL, NULL, &ctx); } /**************************************************************************************************/ @@ -604,9 +705,9 @@ static apr_status_t on_init_agree_tos(md_acme_req_t *req, void *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); - + if (ctx->acme->acct->agreement) { + md_json_setb(1, jpayload, "termsOfServiceAgreed", NULL); + } return md_acme_req_body_init(req, jpayload); } @@ -615,21 +716,14 @@ 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; + if (!strcmp("accepted", agreement) && acme->ca_agreement) { + acme->acct->agreement = acme->ca_agreement; + } + + memset(&ctx, 0, sizeof(ctx)); 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; + return md_acme_POST(acme, acme->acct->url, on_init_agree_tos, acct_upd, NULL, NULL, &ctx); } apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p, @@ -637,32 +731,17 @@ apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p, { apr_status_t rv = APR_SUCCESS; - /* Check if (correct) Terms-of-Service for account were accepted */ + /* We used to really check if the account agreement and the one indicated in meta + * are the very same. However, LE is happy if the account has agreed to a ToS in + * the past and does not require a renewed acceptance. + */ *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); + if (!acme->acct->agreement && acme->ca_agreement) { + if (agreement) { + rv = md_acme_agree(acme, p, acme->ca_agreement); } else { - *prequired = apr_pstrdup(p, tos); + *prequired = acme->ca_agreement; rv = APR_INCOMPLETE; } } diff --git a/modules/md/md_acme_acct.h b/modules/md/md_acme_acct.h index e200da3..b5bba63 100644 --- a/modules/md/md_acme_acct.h +++ b/modules/md/md_acme_acct.h @@ -21,22 +21,32 @@ struct md_acme_req; struct md_json_t; struct md_pkey_t; +#include "md_store.h" /** * An ACME account at an ACME server. */ typedef struct md_acme_acct_t md_acme_acct_t; +typedef enum { + MD_ACME_ACCT_ST_UNKNOWN, + MD_ACME_ACCT_ST_VALID, + MD_ACME_ACCT_ST_DEACTIVATED, + MD_ACME_ACCT_ST_REVOKED, +} md_acme_acct_st; + 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 */ + md_acme_acct_st status; /* status of this account */ 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 */ - + const char *orders; /* URL where certificate orders are found (ACMEv2) */ + const char *eab_kid; /* external account binding keyid used or NULL */ + const char *eab_hmac; /* external account binding hmac used or NULL */ struct md_json_t *registration; /* data from server registration */ - int disabled; }; #define MD_FN_ACCOUNT "account.json" @@ -46,4 +56,93 @@ struct md_acme_acct_t { * are expected to live long, better err on the safe side. */ #define MD_ACME_ACCT_PKEY_BITS 3072 +#define MD_ACME_ACCT_STAGED "staged" + +/** + * Convert an ACME account form/to JSON. + */ +struct md_json_t *md_acme_acct_to_json(md_acme_acct_t *acct, apr_pool_t *p); +apr_status_t md_acme_acct_from_json(md_acme_acct_t **pacct, struct md_json_t *json, apr_pool_t *p); + +/** + * Update the account from the ACME server. + * - Will update acme->acct structure from server on success + * - Will return error status when request failed or account is not known. + */ +apr_status_t md_acme_acct_update(md_acme_t *acme); + +/** + * Update the account and persist changes in the store, if given (and not NULL). + */ +apr_status_t md_acme_acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t *p); + +/** + * 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_for_md(md_acme_t *acme, md_store_t *store, const md_t *md); + +/** + * Find the account id for a given md. + */ +apr_status_t md_acme_acct_id_for_md(const char **pid, md_store_t *store, + md_store_group_t group, const md_t *md, apr_pool_t *p); + +/** + * Create a new account at the ACME server for an MD. The + * new account is the one used by the acme instance afterwards, on success. + */ +apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store, + const md_t *md, apr_pool_t *p); + +apr_status_t md_acme_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme, + const char **pid, struct md_acme_acct_t *acct, + struct md_pkey_t *acct_key); + +/** + * Deactivate the current account at the ACME server. + */ +apr_status_t md_acme_acct_deactivate(md_acme_t *acme, apr_pool_t *p); + +apr_status_t md_acme_acct_load(struct md_acme_acct_t **pacct, struct md_pkey_t **ppkey, + md_store_t *store, md_store_group_t group, + const char *name, apr_pool_t *p); + +/* + * Return != 0 iff the account can be used for the ACME url. + */ +int md_acme_acct_matches_url(md_acme_acct_t *acct, const char *url); + +/* + * Return != 0 iff the account can be used for the MD, including + * its CA url and EAB settings. + */ +int md_acme_acct_matches_md(md_acme_acct_t *acct, const md_t *md); + #endif /* md_acme_acct_h */ diff --git a/modules/md/md_acme_authz.c b/modules/md/md_acme_authz.c index 2b5cbdc..f4579b3 100644 --- a/modules/md/md_acme_authz.c +++ b/modules/md/md_acme_authz.c @@ -32,6 +32,7 @@ #include "md_http.h" #include "md_log.h" #include "md_jws.h" +#include "md_result.h" #include "md_store.h" #include "md_util.h" @@ -46,64 +47,6 @@ md_acme_authz_t *md_acme_authz_create(apr_pool_t *p) 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 */ @@ -133,88 +76,65 @@ static void authz_req_ctx_init(authz_req_ctx *ctx, md_acme_t *acme, 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); -} +/**************************************************************************************************/ +/* Update an existing authorization */ -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) +apr_status_t md_acme_authz_retrieve(md_acme_t *acme, apr_pool_t *p, const char *url, + md_acme_authz_t **pauthz) { - authz_req_ctx *ctx = baton; - const char *location = apr_table_get(hdrs, "location"); - apr_status_t rv = APR_SUCCESS; + md_acme_authz_t *authz; + apr_status_t rv; - (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"); - } + authz = apr_pcalloc(p, sizeof(*authz)); + authz->url = apr_pstrdup(p, url); + rv = md_acme_authz_update(authz, acme, p); + + *pauthz = (APR_SUCCESS == rv)? authz : NULL; 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) +typedef struct { + apr_pool_t *p; + md_acme_authz_t *authz; +} error_ctx_t; + +static int copy_challenge_error(void *baton, size_t index, md_json_t *json) { - apr_status_t rv; - authz_req_ctx ctx; + error_ctx_t *ctx = baton; - (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; + (void)index; + if (md_json_has_key(json, MD_KEY_ERROR, NULL)) { + ctx->authz->error_type = md_json_dups(ctx->p, json, MD_KEY_ERROR, MD_KEY_TYPE, NULL); + ctx->authz->error_detail = md_json_dups(ctx->p, json, MD_KEY_ERROR, MD_KEY_DETAIL, NULL); + ctx->authz->error_subproblems = md_json_dupj(ctx->p, json, MD_KEY_ERROR, MD_KEY_SUBPROBLEMS, NULL); + } + return 1; } -/**************************************************************************************************/ -/* 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) +apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, 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; + error_ctx_t ctx; - (void)store; assert(acme); assert(acme->http); assert(authz); - assert(authz->location); + assert(authz->url); authz->state = MD_ACME_AUTHZ_S_UNKNOWN; json = NULL; + authz->error_type = authz->error_detail = NULL; + authz->error_subproblems = 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) + if (APR_SUCCESS == (rv = md_acme_get_json(&json, acme, authz->url, p)) && (s = md_json_gets(json, MD_KEY_STATUS, NULL))) { - + + authz->domain = md_json_gets(json, MD_KEY_IDENTIFIER, MD_KEY_VALUE, NULL); authz->resource = json; if (!strcmp(s, "pending")) { authz->state = MD_ACME_AUTHZ_S_PENDING; @@ -227,7 +147,10 @@ apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, log_level = MD_LOG_DEBUG; } else if (!strcmp(s, "invalid")) { + ctx.p = p; + ctx.authz = authz; authz->state = MD_ACME_AUTHZ_S_INVALID; + md_json_itera(copy_challenge_error, &ctx, json, MD_KEY_CHALLENGES, NULL); err = "challenge 'invalid'"; } } @@ -239,7 +162,7 @@ apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, 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, + "Exact response was: %s", err, authz->domain, authz->url, json? md_json_writep(json, p, MD_JSON_FMT_COMPACT) : "not available"); } @@ -256,7 +179,12 @@ static md_acme_authz_cha_t *cha_from_json(apr_pool_t *p, size_t index, md_json_t 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); + if (md_json_has_key(json, MD_KEY_URL, NULL)) { /* ACMEv2 */ + cha->uri = md_json_dups(p, json, MD_KEY_URL, NULL); + } + else { /* ACMEv1 */ + 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); @@ -265,13 +193,10 @@ static md_acme_authz_cha_t *cha_from_json(apr_pool_t *p, size_t index, md_json_t static apr_status_t on_init_authz_resp(md_acme_req_t *req, void *baton) { - authz_req_ctx *ctx = baton; md_json_t *jpayload; + (void)baton; 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); } @@ -284,7 +209,7 @@ static apr_status_t authz_http_set(md_acme_t *acme, apr_pool_t *p, const apr_tab (void)p; (void)hdrs; (void)body; - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "updated authz %s", ctx->authz->location); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ctx->p, "updated authz %s", ctx->authz->url); return APR_SUCCESS; } @@ -293,14 +218,13 @@ static apr_status_t setup_key_authz(md_acme_authz_cha_t *cha, md_acme_authz_t *a { 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))) { + if (APR_SUCCESS == (rv = 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)) { @@ -316,136 +240,334 @@ static apr_status_t setup_key_authz(md_acme_authz_cha_t *cha, md_acme_authz_t *a return rv; } -static apr_status_t cha_http_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, +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) + md_pkeys_spec_t *key_specs, + apr_array_header_t *acme_tls_1_domains, const md_t *md, + apr_table_t *env, md_result_t *result, + const char **psetup_token, 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))) { + (void)key_specs; + (void)env; + (void)acme_tls_1_domains; + (void)md; + + if (APR_SUCCESS != (rv = 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)) { + const char *content = apr_psprintf(p, "%s\n", cha->key_authz); 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; + MD_SV_TEXT, (void*)content, 0); notify_server = 1; } if (APR_SUCCESS == rv && notify_server) { authz_req_ctx ctx; - + const char *event; + + /* Raise event that challenge data has been set up before we tell the + ACME server. Clusters might want to distribute it. */ + event = apr_psprintf(p, "challenge-setup:%s:%s", MD_AUTHZ_TYPE_HTTP01, authz->domain); + rv = md_result_raise(result, event, p); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "%s: event '%s' failed. aborting challenge setup", + authz->domain, event); + goto out; + } /* 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); + rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, NULL, &ctx); } out: + *psetup_token = (APR_SUCCESS == rv)? + apr_psprintf(p, "%s:%s", MD_AUTHZ_TYPE_HTTP01, authz->domain) : NULL; return rv; } -static apr_status_t setup_cha_dns(const char **pdns, md_acme_authz_cha_t *cha, apr_pool_t *p) +void tls_alpn01_fnames(apr_pool_t *p, md_pkey_spec_t *kspec, char **keyfn, char **certfn ) { - 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; + *keyfn = apr_pstrcat(p, "acme-tls-alpn-01-", md_pkey_filename(kspec, p), NULL); + *certfn = apr_pstrcat(p, "acme-tls-alpn-01-", md_chain_filename(kspec, p), NULL); } -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) +static apr_status_t cha_tls_alpn_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, + md_acme_t *acme, md_store_t *store, + md_pkeys_spec_t *key_specs, + apr_array_header_t *acme_tls_1_domains, const md_t *md, + apr_table_t *env, md_result_t *result, + const char **psetup_token, apr_pool_t *p) { - md_cert_t *cha_cert; - md_pkey_t *cha_key; - const char *cha_dns; + const char *acme_id, *token; 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; - } + md_data_t data; + int i; - 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; + (void)env; + (void)md; + if (md_array_str_index(acme_tls_1_domains, authz->domain, 0, 0) < 0) { + rv = APR_ENOTIMPL; + if (acme_tls_1_domains->nelts) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "%s: protocol 'acme-tls/1' seems not enabled for this domain, " + "but is enabled for other associated domains. " + "Continuing with fingers crossed.", authz->domain); } - - /* 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); + else { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, + "%s: protocol 'acme-tls/1' seems not enabled for this or " + "any other associated domain. Not attempting challenge " + "type tls-alpn-01.", authz->domain); goto out; } + } + if (APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, ¬ify_server))) { + goto out; + } + + /* Create a "tls-alpn-01" certificate for the domain we want to authenticate. + * The server will need to answer a TLS connection with SNI == authz->domain + * and ALPN protocol "acme-tls/1" with this certificate. + */ + md_data_init_str(&data, cha->key_authz); + rv = md_crypt_sha256_digest_hex(&token, p, &data); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create tls-alpn-01 validation token", + authz->domain); + goto out; + } + acme_id = apr_psprintf(p, "critical,DER:04:20:%s", token); + + /* Each configured key type must be generated to ensure: + * that any fallback certs already given to mod_ssl are replaced. + * We expect that the validation client (at the CA) can deal with at + * least one of them. + */ + + for (i = 0; i < md_pkeys_spec_count(key_specs); ++i) { + char *kfn, *cfn; + md_cert_t *cha_cert; + md_pkey_t *cha_key; + md_pkey_spec_t *key_spec; + + key_spec = md_pkeys_spec_get(key_specs, i); + tls_alpn01_fnames(p, key_spec, &kfn, &cfn); + + rv = md_store_load(store, MD_SG_CHALLENGES, authz->domain, cfn, + MD_SV_CERT, (void**)&cha_cert, p); + if ((APR_SUCCESS == rv && !md_cert_covers_domain(cha_cert, authz->domain)) + || 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-alpn-01 %s challenge key", + authz->domain, md_pkey_spec_name(key_spec)); + goto out; + } + + if (APR_SUCCESS != (rv = md_cert_make_tls_alpn_01(&cha_cert, authz->domain, acme_id, cha_key, + apr_time_from_sec(7 * MD_SECS_PER_DAY), p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create tls-alpn-01 %s challenge cert", + authz->domain, md_pkey_spec_name(key_spec)); + 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); + if (APR_SUCCESS == (rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, kfn, + MD_SV_PKEY, (void*)cha_key, 0))) { + rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, cfn, + MD_SV_CERT, (void*)cha_cert, 0); + } + ++notify_server; } - authz->dir = cha_dns; - notify_server = 1; } if (APR_SUCCESS == rv && notify_server) { authz_req_ctx ctx; - + const char *event; + + /* Raise event that challenge data has been set up before we tell the + ACME server. Clusters might want to distribute it. */ + event = apr_psprintf(p, "challenge-setup:%s:%s", MD_AUTHZ_TYPE_TLSALPN01, authz->domain); + rv = md_result_raise(result, event, p); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "%s: event '%s' failed. aborting challenge setup", + authz->domain, event); + goto out; + } /* 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); + rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, NULL, &ctx); } out: + *psetup_token = (APR_SUCCESS == rv)? + apr_psprintf(p, "%s:%s", MD_AUTHZ_TYPE_TLSALPN01, authz->domain) : NULL; 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); +static apr_status_t cha_dns_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, + md_acme_t *acme, md_store_t *store, + md_pkeys_spec_t *key_specs, + apr_array_header_t *acme_tls_1_domains, const md_t *md, + apr_table_t *env, md_result_t *result, + const char **psetup_token, apr_pool_t *p) +{ + const char *token; + const char * const *argv; + const char *cmdline, *dns01_cmd; + apr_status_t rv; + int exit_code, notify_server; + authz_req_ctx ctx; + md_data_t data; + const char *event; + + (void)store; + (void)key_specs; + (void)acme_tls_1_domains; + + dns01_cmd = md->dns01_cmd; + if (!dns01_cmd) + dns01_cmd = apr_table_get(env, MD_KEY_CMD_DNS01); + if (!dns01_cmd) { + rv = APR_ENOTIMPL; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: dns-01 command not set", + authz->domain); + goto out; + } + + if (APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, ¬ify_server))) { + goto out; + } + + md_data_init_str(&data, cha->key_authz); + rv = md_crypt_sha256_digest64(&token, p, &data); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create dns-01 token for %s", + md->name, authz->domain); + goto out; + } + + cmdline = apr_psprintf(p, "%s setup %s %s", dns01_cmd, authz->domain, token); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "%s: dns-01 setup command: %s", authz->domain, cmdline); + + apr_tokenize_to_argv(cmdline, (char***)&argv, p); + if (APR_SUCCESS != (rv = md_util_exec(p, argv[0], argv, &exit_code))) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, + "%s: dns-01 setup command failed to execute for %s", md->name, authz->domain); + goto out; + } + if (exit_code) { + rv = APR_EGENERAL; + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, + "%s: dns-01 setup command returns %d for %s", md->name, exit_code, authz->domain); + goto out; + } + + /* Raise event that challenge data has been set up before we tell the + ACME server. Clusters might want to distribute it. */ + event = apr_psprintf(p, "challenge-setup:%s:%s", MD_AUTHZ_TYPE_DNS01, authz->domain); + rv = md_result_raise(result, event, p); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "%s: event '%s' failed. aborting challenge setup", + authz->domain, event); + goto out; + } + /* challenge is setup, tell ACME server so it may (re)try verification */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: dns-01 setup succeeded for %s", + md->name, authz->domain); + 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, NULL, &ctx); + +out: + *psetup_token = (APR_SUCCESS == rv)? + apr_psprintf(p, "%s:%s %s", MD_AUTHZ_TYPE_DNS01, authz->domain, token) : NULL; + return rv; +} + +static apr_status_t cha_dns_01_teardown(md_store_t *store, const char *domain, const md_t *md, + apr_table_t *env, apr_pool_t *p) +{ + const char * const *argv; + const char *cmdline, *dns01_cmd, *dns01v; + char *tmp, *s; + apr_status_t rv; + int exit_code; + + (void)store; + + dns01_cmd = md->dns01_cmd; + if (!dns01_cmd) + dns01_cmd = apr_table_get(env, MD_KEY_CMD_DNS01); + if (!dns01_cmd) { + rv = APR_ENOTIMPL; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "%s: dns-01 command not set for %s", + md->name, domain); + goto out; + } + dns01v = apr_table_get(env, MD_KEY_DNS01_VERSION); + if (!dns01v || strcmp(dns01v, "2")) { + /* use older version of teardown args with only domain, remove token */ + tmp = apr_pstrdup(p, domain); + s = strchr(tmp, ' '); + if (s) { + *s = '\0'; + domain = tmp; + } + } + + cmdline = apr_psprintf(p, "%s teardown %s", dns01_cmd, domain); + apr_tokenize_to_argv(cmdline, (char***)&argv, p); + if (APR_SUCCESS != (rv = md_util_exec(p, argv[0], argv, &exit_code)) || exit_code) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, + "%s: dns-01 teardown command failed (exit code=%d) for %s", + md->name, exit_code, domain); + } +out: + return rv; +} + +static apr_status_t cha_teardown_dir(md_store_t *store, const char *domain, const md_t *md, + apr_table_t *env, apr_pool_t *p) +{ + (void)md; + (void)env; + return md_store_purge(store, p, MD_SG_CHALLENGES, domain); +} + +typedef apr_status_t cha_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, + md_acme_t *acme, md_store_t *store, + md_pkeys_spec_t *key_specs, + apr_array_header_t *acme_tls_1_domains, const md_t *md, + apr_table_t *env, md_result_t *result, + const char **psetup_token, apr_pool_t *p); + +typedef apr_status_t cha_teardown(md_store_t *store, const char *domain, const md_t *md, + apr_table_t *env, apr_pool_t *p); typedef struct { const char *name; - cha_starter *start; + cha_setup *setup; + cha_teardown *teardown; } 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 }, + { MD_AUTHZ_TYPE_HTTP01, cha_http_01_setup, cha_teardown_dir }, + { MD_AUTHZ_TYPE_TLSALPN01, cha_tls_alpn_01_setup, cha_teardown_dir }, + { MD_AUTHZ_TYPE_DNS01, cha_dns_01_setup, cha_dns_01_teardown }, }; static const apr_size_t CHA_TYPES_LEN = (sizeof(CHA_TYPES)/sizeof(CHA_TYPES[0])); @@ -481,13 +603,15 @@ static apr_status_t find_type(void *baton, size_t index, md_json_t *json) } 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_array_header_t *challenges, md_pkeys_spec_t *key_specs, + apr_array_header_t *acme_tls_1_domains, const md_t *md, + apr_table_t *env, apr_pool_t *p, const char **psetup_token, + md_result_t *result) { apr_status_t rv; - int i; + int i, j; cha_find_ctx fctx; - + assert(acme); assert(authz); assert(authz->resource); @@ -495,229 +619,98 @@ apr_status_t md_acme_authz_respond(md_acme_authz_t *authz, md_acme_t *acme, md_s fctx.p = p; fctx.accepted = NULL; - /* Look in the order challenge types are defined */ - for (i = 0; i < challenges->nelts && !fctx.accepted; ++i) { + /* Look in the order challenge types are defined: + * - if they are offered by the CA, try to set it up + * - if setup was successful, we are done and the CA will evaluate us + * - if setup failed, continue to look for another supported challenge type + * - if there is no overlap in types, tell the user that she has to configure + * either more types (dns, tls-alpn-01), make ports available or refrain + * from using wildcard domains when dns is not available. etc. + * - if there was an overlap, but no setup was successful, report that. We + * will retry this, maybe the failure is temporary (e.g. command to setup DNS + */ + md_result_printf(result, 0, "%s: selecting suitable authorization challenge " + "type, this domain supports %s", + authz->domain, apr_array_pstrcat(p, challenges, ' ')); + rv = APR_ENOTIMPL; + *psetup_token = NULL; + for (i = 0; i < challenges->nelts; ++i) { fctx.type = APR_ARRAY_IDX(challenges, i, const char *); + fctx.accepted = NULL; md_json_itera(find_type, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, + "%s: challenge type '%s' for %s: %s", + authz->domain, fctx.type, md->name, + fctx.accepted? "maybe acceptable" : "not applicable"); + + if (fctx.accepted) { + for (j = 0; j < (int)CHA_TYPES_LEN; ++j) { + if (!apr_strnatcasecmp(CHA_TYPES[j].name, fctx.accepted->type)) { + md_result_activity_printf(result, "Setting up challenge '%s' for domain %s", + fctx.accepted->type, authz->domain); + rv = CHA_TYPES[j].setup(fctx.accepted, authz, acme, store, key_specs, + acme_tls_1_domains, md, env, result, + psetup_token, p); + if (APR_SUCCESS == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "%s: set up challenge '%s' for %s", + authz->domain, fctx.accepted->type, md->name); + goto out; + } + md_result_printf(result, rv, "error setting up challenge '%s' for %s, " + "for domain %s, looking for other option", + fctx.accepted->type, authz->domain, md->name); + md_result_log(result, MD_LOG_INFO); + } + } + } } - if (!fctx.accepted) { +out: + if (!fctx.accepted || APR_ENOTIMPL == rv) { 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).", + md_result_printf(result, rv, "None of offered challenge types for domain %s are supported. " + "The server offered '%s' and available are: '%s'.", authz->domain, apr_array_pstrcat(p, fctx.offered, ' '), - apr_array_pstrcat(p, challenges, ' '), - authz->location); - return rv; + apr_array_pstrcat(p, challenges, ' ')); + result->problem = "challenge-mismatch"; + md_result_log(result, MD_LOG_ERR); } - - 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); - } + else if (APR_SUCCESS != rv) { + fctx.offered = apr_array_make(p, 5, sizeof(const char*)); + md_json_itera(collect_offered, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL); + md_result_printf(result, rv, "None of the offered challenge types %s offered " + "for domain %s could be setup successfully. Please check the " + "log for errors.", authz->domain, + apr_array_pstrcat(p, fctx.offered, ' ')); + result->problem = "challenge-setup-failure"; + md_result_log(result, MD_LOG_ERR); } - - 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) +apr_status_t md_acme_authz_teardown(struct md_store_t *store, const char *token, + const md_t *md, apr_table_t *env, 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; + char *challenge, *domain; 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); + + if (strchr(token, ':')) { + challenge = apr_pstrdup(p, token); + domain = strchr(challenge, ':'); + *domain = '\0'; domain++; + for (i = 0; i < (int)CHA_TYPES_LEN; ++i) { + if (!apr_strnatcasecmp(CHA_TYPES[i].name, challenge)) { + if (CHA_TYPES[i].teardown) { + return CHA_TYPES[i].teardown(store, domain, md, env, p); + } + break; } } } - 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); + return APR_SUCCESS; } diff --git a/modules/md/md_acme_authz.h b/modules/md/md_acme_authz.h index aa33f23..d74beeb 100644 --- a/modules/md/md_acme_authz.h +++ b/modules/md/md_acme_authz.h @@ -18,19 +18,22 @@ #define mod_md_md_acme_authz_h struct apr_array_header_t; +struct apr_table_t; struct md_acme_t; struct md_acme_acct_t; struct md_json_t; struct md_store_t; struct md_pkey_spec_t; +struct md_result_t; typedef struct md_acme_challenge_t md_acme_challenge_t; /**************************************************************************************************/ /* authorization request for a specific domain name */ +#define MD_AUTHZ_TYPE_DNS01 "dns-01" #define MD_AUTHZ_TYPE_HTTP01 "http-01" -#define MD_AUTHZ_TYPE_TLSSNI01 "tls-sni-01" +#define MD_AUTHZ_TYPE_TLSALPN01 "tls-alpn-01" typedef enum { MD_ACME_AUTHZ_S_UNKNOWN, @@ -43,62 +46,34 @@ typedef struct md_acme_authz_t md_acme_authz_t; struct md_acme_authz_t { const char *domain; - const char *location; - const char *dir; + const char *url; md_acme_authz_state_t state; apr_time_t expires; + const char *error_type; + const char *error_detail; + const struct md_json_t *error_subproblems; 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" +void tls_alpn01_fnames(apr_pool_t *p, struct md_pkey_spec_t *kspec, char **keyfn, char **certfn ); 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_retrieve(md_acme_t *acme, apr_pool_t *p, const char *url, + md_acme_authz_t **pauthz); +apr_status_t md_acme_authz_update(md_acme_authz_t *authz, struct md_acme_t *acme, 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); + struct md_pkeys_spec_t *key_spec, + apr_array_header_t *acme_tls_1_domains, const md_t *md, + struct apr_table_t *env, + apr_pool_t *p, const char **setup_token, + struct md_result_t *result); + +apr_status_t md_acme_authz_teardown(struct md_store_t *store, const char *setup_token, + const md_t *md, struct apr_table_t *env, apr_pool_t *p); #endif /* md_acme_authz_h */ diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c index ba4e865..4bb04f3 100644 --- a/modules/md/md_acme_drive.c +++ b/modules/md/md_acme_drive.c @@ -29,6 +29,7 @@ #include "md_jws.h" #include "md_http.h" #include "md_log.h" +#include "md_result.h" #include "md_reg.h" #include "md_store.h" #include "md_util.h" @@ -36,315 +37,160 @@ #include "md_acme.h" #include "md_acme_acct.h" #include "md_acme_authz.h" +#include "md_acme_order.h" -typedef struct { - md_proto_driver_t *driver; - - const char *phase; - int complete; +#include "md_acme_drive.h" +#include "md_acmev2_drive.h" - 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; +/**************************************************************************************************/ +/* account setup */ + +static apr_status_t use_staged_acct(md_acme_t *acme, struct md_store_t *store, + const md_t *md, apr_pool_t *p) +{ + md_acme_acct_t *acct; + md_pkey_t *pkey; + apr_status_t rv; - apr_array_header_t *ca_challenges; - md_acme_authz_set_t *authz_set; - apr_interval_time_t authz_monitor_timeout; + if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey, store, + MD_SG_STAGING, md->name, acme->p))) { + acme->acct_id = NULL; + acme->acct = acct; + acme->acct_key = pkey; + rv = md_acme_acct_validate(acme, NULL, p); + } + return rv; +} + +static apr_status_t save_acct_staged(md_acme_t *acme, md_store_t *store, + const char *md_name, apr_pool_t *p) +{ + md_json_t *jacct; + apr_status_t rv; - const char *csr_der_64; - apr_interval_time_t cert_poll_timeout; + jacct = md_acme_acct_to_json(acme->acct, p); -} md_acme_driver_t; - -/**************************************************************************************************/ -/* account setup */ + 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; +} -static apr_status_t ad_set_acct(md_proto_driver_t *d) +apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result) { md_acme_driver_t *ad = d->baton; md_t *md = ad->md; apr_status_t rv = APR_SUCCESS; - int update = 0, acct_installed = 0; + int update_md = 0, update_acct = 0; + + md_result_activity_printf(result, "Selecting account to use for %s", d->md->name); + md_acme_clear_acct(ad->acme); - 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))) { + if (APR_SUCCESS == (rv = use_staged_acct(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; + else if (!APR_STATUS_IS_ENOENT(rv)) { + goto leave; } /* Get an account for the ACME server for this MD */ - if (md->ca_account && !acct_installed) { + if (!ad->acme->acct && md->ca_account) { 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); + rv = md_acme_use_acct_for_md(ad->acme, d->store, d->p, md->ca_account, md); 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; + update_md = 1; + } + else if (APR_SUCCESS != rv) { + goto leave; } } - if (APR_SUCCESS == rv && !md->ca_account) { + if (!ad->acme->acct && !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_acme_find_acct_for_md(ad->acme, d->store, md))) { + md->ca_account = md_acme_acct_id_get(ad->acme); + update_md = 1; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: using account %s (id=%s)", + d->proto->protocol, ad->acme->acct->url, md->ca_account); } } - if (APR_SUCCESS == rv && !md->ca_account) { - /* 2.2 No local account exists, create a new one */ + if (!ad->acme->acct) { + /* No account staged, no suitable found in store, register a new one */ + md_result_activity_printf(result, "Creating new ACME account for %s", d->md->name); 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; + md_result_printf(result, rv, "No contact information is available for MD %s. " + "Configure one using the MDContactEmail or ServerAdmin directive.", md->name); + md_result_log(result, MD_LOG_ERR); + goto leave; } - - 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; + + /* ACMEv1 allowed registration of accounts without accepted Terms-of-Service. + * ACMEv2 requires it. Fail early in this case with a meaningful error message. + */ + if (!md->ca_agreement) { + md_result_printf(result, APR_EINVAL, + "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 accepted\" " + "in your Apache. Then (graceful) restart the server to activate.", + ad->acme->ca_agreement); + md_result_log(result, MD_LOG_ERR); + rv = result->status; + goto leave; } - } -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 (ad->acme->eab_required && (!md->ca_eab_kid || !strcmp("none", md->ca_eab_kid))) { + md_result_printf(result, APR_EINVAL, + "the CA requires 'External Account Binding' which is not " + "configured. This means you need to obtain a 'Key ID' and a " + "'HMAC' from the CA and configure that using the " + "MDExternalAccountBinding directive in your config. " + "The creation of a new ACME account will most likely fail, " + "but an attempt is made anyway.", + ad->acme->ca_agreement); + md_result_log(result, MD_LOG_INFO); } - 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; + rv = md_acme_acct_register(ad->acme, d->store, md, d->p); + if (APR_SUCCESS != rv) { + if (APR_SUCCESS != ad->acme->last->status) { + md_result_dup(result, ad->acme->last); + md_result_log(result, MD_LOG_ERR); } - } - } - - /* 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; + goto leave; } - 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; - } + md->ca_account = NULL; + update_md = 1; + update_acct = 1; } - 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); +leave: + /* Persist MD changes in STAGING, so we pick them up on next run */ + if (APR_SUCCESS == rv && update_md) { + rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); } - 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; - } - } + /* Persist account changes in STAGING, so we pick them up on next run */ + if (APR_SUCCESS == rv && update_acct) { + rv = save_acct_staged(ad->acme, d->store, md->name, d->p); } 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 */ @@ -352,41 +198,52 @@ 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) { + ad->chain_up_link = md_link_find_relation(headers, d->p, "up"); + if (ad->chain_up_link) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, - "server reports up link as %s", ad->next_up_link); + "server reports up link as %s", ad->chain_up_link); } } -static apr_status_t read_http_cert(md_cert_t **pcert, apr_pool_t *p, +static apr_status_t add_http_certs(apr_array_header_t *chain, apr_pool_t *p, const md_http_response_t *res) { apr_status_t rv = APR_SUCCESS; + const char *ct; - if (APR_SUCCESS != (rv = md_cert_read_http(pcert, p, res)) + ct = apr_table_get(res->headers, "Content-Type"); + ct = md_util_parse_ct(res->req->pool, ct); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p, + "parse certs from %s -> %d (%s)", res->req->url, res->status, ct); + if (ct && !strcmp("application/x-pkcs7-mime", ct)) { + /* this looks like a root cert and we do not want those in our chain */ + goto out; + } + + /* Lets try to read one or more certificates */ + if (APR_SUCCESS != (rv = md_cert_chain_read_http(chain, 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); } +out: return rv; } -static apr_status_t on_got_cert(md_acme_t *acme, const md_http_response_t *res, void *baton) +static apr_status_t on_add_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; + int count; (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); - } + count = ad->cred->chain->nelts; + if (APR_SUCCESS == (rv = add_http_certs(ad->cred->chain, d->p, res))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%d certs parsed", + ad->cred->chain->nelts - count); + get_up_link(d, res->headers); } return rv; } @@ -397,19 +254,21 @@ static apr_status_t get_cert(void *baton, int attempt) 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); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, d->p, "retrieving cert from %s", + ad->order->certificate); + return md_acme_GET(ad->acme, ad->order->certificate, NULL, NULL, on_add_cert, NULL, d); } -static apr_status_t ad_cert_poll(md_proto_driver_t *d, int only_once) +apr_status_t md_acme_drive_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); + assert(ad->order); + assert(ad->order->certificate); - ad->phase = "poll certificate"; if (only_once) { rv = get_cert(d, 0); } @@ -417,12 +276,12 @@ static apr_status_t ad_cert_poll(md_proto_driver_t *d, int only_once) 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); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "poll for cert at %s", ad->order->certificate); return rv; } /**************************************************************************************************/ -/* cert setup */ +/* order finalization */ static apr_status_t on_init_csr_req(md_acme_req_t *req, void *baton) { @@ -431,7 +290,6 @@ static apr_status_t on_init_csr_req(md_acme_req_t *req, void *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); @@ -441,34 +299,39 @@ static apr_status_t csr_req(md_acme_t *acme, const md_http_response_t *res, void { md_proto_driver_t *d = baton; md_acme_driver_t *ad = d->baton; + const char *location; + md_cert_t *cert; apr_status_t rv = APR_SUCCESS; (void)acme; - ad->md->cert_url = apr_table_get(res->headers, "location"); - if (!ad->md->cert_url) { + location = apr_table_get(res->headers, "location"); + if (!location) { 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))) { + ad->order->certificate = apr_pstrdup(d->p, location); + if (APR_SUCCESS != (rv = md_acme_order_save(d->store, d->p, MD_SG_STAGING, + d->md->name, ad->order, 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); + "%s: saving cert url %s", d->md->name, location); 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); - } + ad->chain_up_link = NULL; + if (APR_SUCCESS == (rv = md_cert_read_http(&cert, d->p, res))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "cert parsed"); + apr_array_clear(ad->cred->chain); + APR_ARRAY_PUSH(ad->cred->chain, md_cert_t*) = cert; + 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); + if (location) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "cert not in response, need to poll %s", location); + } } return rv; @@ -477,6 +340,7 @@ static apr_status_t csr_req(md_acme_t *acme, const md_http_response_t *res, void /** * Pre-Req: all domains have been validated by the ACME server, e.g. all have AUTHZ * resources that have status 'valid' + * - acme_driver->cred keeps the credentials to setup (key spec) * - Setup private key, if not already there * - Generate a CSR with org, contact, etc * - Optionally enable must-staple OCSP extension @@ -487,38 +351,41 @@ static apr_status_t csr_req(md_acme_t *acme, const md_http_response_t *res, void * - GET cert chain * - store cert chain */ -static apr_status_t ad_setup_certificate(md_proto_driver_t *d) +apr_status_t md_acme_drive_setup_cred_chain(md_proto_driver_t *d, md_result_t *result) { md_acme_driver_t *ad = d->baton; + md_pkey_spec_t *spec; 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); + md_result_activity_printf(result, "Finalizing order for %s", ad->md->name); + + assert(ad->cred); + spec = ad->cred->spec; + + rv = md_pkey_load(d->store, MD_SG_STAGING, d->md->name, spec, &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); + if (APR_SUCCESS == (rv = md_pkey_gen(&privkey, d->p, spec))) { + rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, d->md->name, spec, 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); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "%s: generate %s privkey", d->md->name, md_pkey_spec_name(spec)); } + if (APR_SUCCESS != rv) goto leave; + + md_result_activity_printf(result, "Creating %s CSR", md_pkey_spec_name(spec)); + rv = md_cert_req_create(&ad->csr_der_64, d->md->name, ad->domains, + ad->md->must_staple, privkey, d->p); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: create %s CSR", + d->md->name, md_pkey_spec_name(spec)); + if (APR_SUCCESS != rv) goto leave; + + md_result_activity_printf(result, "Submitting %s CSR to CA", md_pkey_spec_name(spec)); + assert(ad->order->finalize); + rv = md_acme_POST(ad->acme, ad->order->finalize, on_init_csr_req, NULL, csr_req, NULL, d); - if (APR_SUCCESS == rv) { - if (!ad->cert) { - rv = ad_cert_poll(d, 0); - } - } +leave: + md_acme_report_result(ad->acme, rv, result); return rv; } @@ -530,22 +397,19 @@ static apr_status_t on_add_chain(md_acme_t *acme, const md_http_response_t *res, 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"); + ct = md_util_parse_ct(res->req->pool, ct); 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))) { + if (APR_SUCCESS == (rv = add_http_certs(ad->cred->chain, 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); - } + get_up_link(d, res->headers); } return rv; } @@ -557,19 +421,32 @@ static apr_status_t get_chain(void *baton, int attempt) const char *prev_link = NULL; apr_status_t rv = APR_SUCCESS; - while (APR_SUCCESS == rv && ad->chain->nelts < 10) { - int nelts = ad->chain->nelts; + while (APR_SUCCESS == rv && ad->cred->chain->nelts < 10) { + int nelts = ad->cred->chain->nelts; - if (ad->next_up_link && (!prev_link || strcmp(prev_link, ad->next_up_link))) { - prev_link = ad->next_up_link; + if (ad->chain_up_link && (!prev_link || strcmp(prev_link, ad->chain_up_link))) { + prev_link = ad->chain_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); + "next chain cert at %s", ad->chain_up_link); + rv = md_acme_GET(ad->acme, ad->chain_up_link, NULL, NULL, on_add_chain, NULL, d); - if (APR_SUCCESS == rv && nelts == ad->chain->nelts) { + if (APR_SUCCESS == rv && nelts == ad->cred->chain->nelts) { break; } + else if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "error retrieving certificate from %s", ad->chain_up_link); + return rv; + } + } + else if (ad->cred->chain->nelts <= 1) { + /* This cannot be the complete chain (no one signs new web certs with their root) + * and we did not see a "Link: ...rel=up", so we do not know how to continue. */ + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "no link header 'up' for new certificate, unable to retrieve chain"); + rv = APR_EINVAL; + break; } else { rv = APR_SUCCESS; @@ -577,63 +454,103 @@ static apr_status_t get_chain(void *baton, int attempt) } } md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, - "got chain with %d certs (%d. attempt)", ad->chain->nelts, attempt); + "got chain with %d certs (%d. attempt)", ad->cred->chain->nelts, attempt); return rv; } -static apr_status_t ad_chain_install(md_proto_driver_t *d) +static apr_status_t ad_chain_retrieve(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; + /* This may be called repeatedly and needs to progress. The relevant state is in + * ad->cred->chain the certificate chain, starting with the new cert for the md + * ad->order->certificate the url where ACME offers us the new md certificate. This may + * be a single one or even the complete chain + * ad->chain_up_link in case the last certificate retrieval did not end the chain, + * the link header with relation "up" gives us the location + * for the next cert in the chain + */ + if (md_array_is_empty(ad->cred->chain)) { + /* Need to start at the order */ + ad->chain_up_link = NULL; + if (!ad->order) { + rv = APR_EGENERAL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "%s: asked to retrieve chain, but no order in context", d->md->name); + goto out; } - if (!ad->next_up_link) { + if (!ad->order->certificate) { + rv = APR_EGENERAL; 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; + "%s: asked to retrieve chain, but no certificate url part of order", d->md->name); + goto out; + } + + if (APR_SUCCESS != (rv = md_acme_drive_cert_poll(d, 0))) { + goto out; } } - 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"); - } + rv = md_util_try(get_chain, d, 0, ad->cert_poll_timeout, 0, 0, 0); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain retrieved"); + +out: return rv; } /**************************************************************************************************/ /* ACME driver init */ -static apr_status_t acme_driver_init(md_proto_driver_t *d) +static apr_status_t acme_driver_preload_init(md_proto_driver_t *d, md_result_t *result) { md_acme_driver_t *ad; - apr_status_t rv = APR_SUCCESS; - + md_credentials_t *cred; + int i; + + md_result_set(result, APR_SUCCESS, NULL); + ad = apr_pcalloc(d->p, sizeof(*ad)); d->baton = ad; - ad->driver = d; + ad->driver = d; ad->authz_monitor_timeout = apr_time_from_sec(30); ad->cert_poll_timeout = apr_time_from_sec(30); + ad->ca_challenges = apr_array_make(d->p, 3, sizeof(const char*)); + + /* We want to obtain credentials (key+certificate) for every key spec in this MD */ + ad->creds = apr_array_make(d->p, md_pkeys_spec_count(d->md->pks), sizeof(md_credentials_t*)); + for (i = 0; i < md_pkeys_spec_count(d->md->pks); ++i) { + cred = apr_pcalloc(d->p, sizeof(*cred)); + cred->spec = md_pkeys_spec_get(d->md->pks, i); + cred->chain = apr_array_make(d->p, 5, sizeof(md_cert_t*)); + APR_ARRAY_PUSH(ad->creds, md_credentials_t*) = cred; + } + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, result->status, d->p, + "%s: init_base driver", d->md->name); + return result->status; +} + +static apr_status_t acme_driver_init(md_proto_driver_t *d, md_result_t *result) +{ + md_acme_driver_t *ad; + int dis_http, dis_https, dis_alpn_acme, dis_dns; + const char *challenge; + + acme_driver_preload_init(d, result); + md_result_set(result, APR_SUCCESS, NULL); + if (APR_SUCCESS != result->status) goto leave; + + ad = d->baton; /* 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); + challenge = apr_table_get(d->env, MD_KEY_CHALLENGE); + if (challenge) { + APR_ARRAY_PUSH(ad->ca_challenges, const char*) = apr_pstrdup(d->p, challenge); } else if (d->md->ca_challenges && d->md->ca_challenges->nelts > 0) { /* pre-configured set for this managed domain */ @@ -641,56 +558,119 @@ static apr_status_t acme_driver_init(md_proto_driver_t *d) } 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_TLSALPN01; 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); - } + APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_DNS01; + + if (!d->can_http && !d->can_https + && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0, 0) < 0) { + md_result_printf(result, APR_EGENERAL, + "the server seems neither reachable via http (port 80) nor https (port 443). " + "Please look at the MDPortMap configuration directive on how to correct this. " + "The ACME protocol needs at least one of those so the CA can talk to the server " + "and verify a domain ownership. Alternatively, you may configure support " + "for the %s challenge directive.", MD_AUTHZ_TYPE_DNS01); + goto leave; + } - 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; + dis_http = dis_https = dis_alpn_acme = dis_dns = 0; + if (!d->can_http && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_HTTP01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_HTTP01, 0); + dis_http = 1; + } + if (!d->can_https && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0); + dis_https = 1; + } + if (apr_is_empty_array(d->md->acme_tls_1_domains) + && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0); + dis_alpn_acme = 1; + } + if (!apr_table_get(d->env, MD_KEY_CMD_DNS01) + && NULL == d->md->dns01_cmd + && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0); + dis_dns = 1; + } + + if (apr_is_empty_array(ad->ca_challenges)) { + md_result_printf(result, APR_EGENERAL, + "None of the ACME challenge methods configured for this domain are suitable.%s%s%s%s", + dis_http? " The http: challenge 'http-01' is disabled because the server seems not reachable on public port 80." : "", + dis_https? " The https: challenge 'tls-alpn-01' is disabled because the server seems not reachable on public port 443." : "", + dis_alpn_acme? " The https: challenge 'tls-alpn-01' is disabled because the Protocols configuration does not include the 'acme-tls/1' protocol." : "", + dis_dns? " The DNS challenge 'dns-01' is disabled because the directive 'MDChallengeDns01' is not configured." : "" + ); + goto leave; + } } - - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, d->p, "%s: init driver", d->md->name); - - return rv; + + md_result_printf(result, 0, "MDomain %s initialized with support for ACME challenges %s", + d->md->name, apr_array_pstrcat(d->p, ad->ca_challenges, ' ')); + +leave: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, result->status, d->p, "%s: init driver", d->md->name); + return result->status; } /**************************************************************************************************/ /* ACME staging */ -static apr_status_t acme_stage(md_proto_driver_t *d) +static apr_status_t load_missing_creds(md_proto_driver_t *d) +{ + md_acme_driver_t *ad = d->baton; + md_credentials_t *cred; + apr_array_header_t *chain; + int i, complete; + apr_status_t rv; + + complete = 1; + for (i = 0; i < ad->creds->nelts; ++i) { + rv = APR_SUCCESS; + cred = APR_ARRAY_IDX(ad->creds, i, md_credentials_t*); + if (!cred->pkey) { + rv = md_pkey_load(d->store, MD_SG_STAGING, d->md->name, cred->spec, &cred->pkey, d->p); + } + if (APR_SUCCESS == rv && md_array_is_empty(cred->chain)) { + rv = md_pubcert_load(d->store, MD_SG_STAGING, d->md->name, cred->spec, &chain, d->p); + if (APR_SUCCESS == rv) { + apr_array_cat(cred->chain, chain); + } + } + if (APR_SUCCESS == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, d->p, "%s: credentials staged for %s certificate", + d->md->name, md_pkey_spec_name(cred->spec)); + } + else { + complete = 0; + } + } + return complete? APR_SUCCESS : APR_EAGAIN; +} + +static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result) { 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, ' ')); + apr_time_t now, t, t2; + md_credentials_t *cred; + const char *ca_effective = NULL; + char ts[APR_RFC822_DATE_LEN]; + int i, first = 0; + + if (!d->md->ca_urls || d->md->ca_urls->nelts <= 0) { + /* No CA defined? This is checked in several other places, but lets be sure */ + md_result_printf(result, APR_INCOMPLETE, + "The managed domain %s is missing MDCertificateAuthority", d->md->name); + goto out; } + /* When not explicitly told to reset, we check the existing data. If + * it is incomplete or old, we trigger the reset for a clean start. */ if (!reset_staging) { + md_result_activity_setn(result, "Checking staging area"); 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? */ @@ -702,318 +682,420 @@ static apr_status_t acme_stage(md_proto_driver_t *d) 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"); } - + + /* What CA are we using this time? */ + if (ad->md && ad->md->ca_effective) { + /* There was one chosen on the previous run. Do we stick to it? */ + ca_effective = ad->md->ca_effective; + if (d->md->ca_urls->nelts > 1 && d->attempt >= d->retry_failover) { + /* We have more than one CA to choose from and this is the (at least) + * third attempt with the same CA. Let's switch to the next one. */ + int last_idx = md_array_str_index(d->md->ca_urls, ca_effective, 0, 1); + if (last_idx >= 0) { + int next_idx = (last_idx+1) % d->md->ca_urls->nelts; + ca_effective = APR_ARRAY_IDX(d->md->ca_urls, next_idx, const char*); + } + else { + /* not part of current configuration? */ + ca_effective = NULL; + } + /* switching CA means we need to wipe the staging area */ + reset_staging = 1; + } + } + + if (!ca_effective) { + /* None chosen yet, pick the first one configured */ + ca_effective = APR_ARRAY_IDX(d->md->ca_urls, 0, const char*); + } + + 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, attempt=%d, acme=%s, challenges='%s'", + d->md->name, d->md->state, d->attempt, ca_effective, + apr_array_pstrcat(d->p, ad->ca_challenges, ' ')); + } + if (reset_staging) { + md_result_activity_setn(result, "Resetting staging area"); /* reset the staging area for this domain */ rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "%s: reset staging area", d->md->name); if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) { - return rv; + md_result_printf(result, rv, "resetting staging area"); + goto out; } rv = APR_SUCCESS; ad->md = NULL; + ad->order = 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; + md_result_activity_setn(result, "Assessing current status"); + if (ad->md && ad->md->state == MD_S_MISSING_INFORMATION) { + /* ToS agreement is missing. It makes no sense to drive this MD further */ + md_result_printf(result, APR_INCOMPLETE, + "The managed domain %s is missing required information", d->md->name); 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; - } + if (ad->md && APR_SUCCESS == load_missing_creds(d)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: all credentials staged", d->md->name); + goto ready; } - /* 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; + /* Need to renew */ + if (!ad->md || !md_array_str_eq(ad->md->ca_urls, d->md->ca_urls, 1)) { + md_result_activity_printf(result, "Resetting staging for %s", d->md->name); + /* re-initialize staging */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 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->ca_effective = ca_effective; + ad->md->ca_account = NULL; + ad->order = NULL; + rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Saving MD information in staging area."); + md_result_log(result, MD_LOG_ERR); + goto out; + } + } + if (!ad->domains) { + ad->domains = md_dns_make_minimal(d->p, ad->md->domains); } - 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); - } + md_result_activity_printf(result, "Contacting ACME server for %s at %s", + d->md->name, ca_effective); + if (APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, ca_effective, + d->proxy_url, d->ca_file))) { + md_result_printf(result, rv, "setup ACME communications"); + md_result_log(result, MD_LOG_ERR); + goto out; + } + if (APR_SUCCESS != (rv = md_acme_setup(ad->acme, result))) { + md_result_log(result, MD_LOG_ERR); + goto out; + } - 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 (APR_SUCCESS != load_missing_creds(d)) { + for (i = 0; i < ad->creds->nelts; ++i) { + ad->cred = APR_ARRAY_IDX(ad->creds, i, md_credentials_t*); + if (!ad->cred->pkey || md_array_is_empty(ad->cred->chain)) { + md_result_activity_printf(result, "Driving ACME to renew %s certificate for %s", + md_pkey_spec_name(ad->cred->spec),d->md->name); + /* The process of setting up challenges and verifying domain + * names differs between ACME versions. */ + switch (MD_ACME_VERSION_MAJOR(ad->acme->version)) { + case 1: + md_result_printf(result, APR_EINVAL, + "ACME server speaks version 1, an obsolete version of the ACME " + "protocol that is no longer supported."); + rv = result->status; + break; + default: + /* In principle, we only know ACME version 2. But we assume + that a new protocol which announces a directory with all members + from version 2 will act backward compatible. + This is, of course, an assumption... + */ + rv = md_acmev2_drive_renew(ad, d, result); + break; } - } - - /* 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; + if (APR_SUCCESS != rv) goto out; + + if (md_array_is_empty(ad->cred->chain) || ad->chain_up_link) { + md_result_activity_printf(result, "Retrieving %s certificate chain for %s", + md_pkey_spec_name(ad->cred->spec), d->md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, + "%s: retrieving %s certificate chain", + d->md->name, md_pkey_spec_name(ad->cred->spec)); + rv = ad_chain_retrieve(d); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Unable to retrieve %s certificate chain.", + md_pkey_spec_name(ad->cred->spec)); + goto out; + } + + if (!md_array_is_empty(ad->cred->chain)) { + + if (!ad->cred->pkey) { + rv = md_pkey_load(d->store, MD_SG_STAGING, d->md->name, ad->cred->spec, &ad->cred->pkey, d->p); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Loading the private key."); + goto out; + } + } + + if (ad->cred->pkey) { + rv = md_check_cert_and_pkey(ad->cred->chain, ad->cred->pkey); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Certificate and private key do not match."); + + /* Delete the order */ + md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env); + + goto out; + } + } + + rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, d->md->name, + ad->cred->spec, ad->cred->chain, 0); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Saving new %s certificate chain.", + md_pkey_spec_name(ad->cred->spec)); + goto out; + } + } } - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, - "%s: received certificate", d->md->name); + + /* Clean up the order, so the next pkey spec sets up a new one */ + md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env); } - } + } + + + /* As last step, cleanup any order we created so that challenge data + * may be removed asap. */ + md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env); + + /* first time this job ran through */ + first = 1; +ready: + md_result_activity_setn(result, NULL); + /* we should have the complete cert chain now */ + assert(APR_SUCCESS == load_missing_creds(d)); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "%s: certificates ready, activation delay set to %s", + d->md->name, md_duration_format(d->p, d->activation_delay)); + + /* determine when it should be activated */ + t = apr_time_now(); + for (i = 0; i < ad->creds->nelts; ++i) { + cred = APR_ARRAY_IDX(ad->creds, i, md_credentials_t*); + t2 = md_cert_get_not_before(APR_ARRAY_IDX(cred->chain, 0, md_cert_t*)); + if (t2 > t) t = t2; + } + md_result_delay_set(result, t); + + /* If the existing MD is complete and un-expired, delay the activation + * to 24 hours after new cert is valid (if there is enough time left), so + * that cients with skewed clocks do not see a problem. */ + now = apr_time_now(); + if (d->md->state == MD_S_COMPLETE) { + apr_time_t valid_until, delay_activation; - 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); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "%s: state is COMPLETE, checking existing certificates", d->md->name); + valid_until = md_reg_valid_until(d->reg, d->md, d->p); + if (d->activation_delay < 0) { + /* special simulation for test case */ + if (first) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "%s: delay ready_at to now+1s", d->md->name); + md_result_delay_set(result, apr_time_now() + apr_time_from_sec(1)); + } } - - 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; + else if (valid_until > now) { + delay_activation = d->activation_delay; + if (delay_activation > (valid_until - now)) { + delay_activation = (valid_until - now); } + md_result_delay_set(result, result->ready_at + delay_activation); } } -out: + + /* There is a full set staged, to be loaded */ + apr_rfc822_date(ts, result->ready_at); + if (result->ready_at > now) { + md_result_printf(result, APR_SUCCESS, + "The certificate for the managed domain has been renewed successfully and can " + "be used from %s on.", ts); + } + else { + md_result_printf(result, APR_SUCCESS, + "The certificate for the managed domain has been renewed successfully and can " + "be used (valid since %s). A graceful server restart now is recommended.", ts); + } + +out: return rv; } -static apr_status_t acme_driver_stage(md_proto_driver_t *d) +static apr_status_t acme_driver_renew(md_proto_driver_t *d, md_result_t *result) { - 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); + rv = acme_renew(d, result); + md_result_log(result, MD_LOG_DEBUG); 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) +static apr_status_t acme_preload(md_proto_driver_t *d, md_store_group_t load_group, + const char *name, md_result_t *result) { apr_status_t rv; - md_pkey_t *privkey, *acct_key; + md_pkey_t *acct_key; md_t *md; - apr_array_header_t *pubcert; + md_pkey_spec_t *pkspec; + md_credentials_t *creds; + apr_array_header_t *all_creds; struct md_acme_acct_t *acct; + const char *id; + int i; - 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. + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: preload start", name); + /* Load data from MD_SG_STAGING and save it into "load_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 + * 4. Once "load_group" is complete an ok, 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_load(d->store, MD_SG_STAGING, name, &md, d->p))) { + md_result_set(result, rv, "loading staged md.json"); + goto leave; } - 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; + if (!md->ca_effective) { + rv = APR_ENOENT; + md_result_set(result, rv, "effective CA url not set"); + goto leave; } + all_creds = apr_array_make(d->p, 5, sizeof(md_credentials_t*)); + for (i = 0; i < md_pkeys_spec_count(md->pks); ++i) { + pkspec = md_pkeys_spec_get(md->pks, i); + if (APR_SUCCESS != (rv = md_creds_load(d->store, MD_SG_STAGING, name, pkspec, &creds, d->p))) { + md_result_printf(result, rv, "loading staged credentials #%d", i); + goto leave; + } + if (!creds->chain) { + rv = APR_ENOENT; + md_result_printf(result, rv, "no certificate in staged credentials #%d", i); + goto leave; + } + if (APR_SUCCESS != (rv = md_check_cert_and_pkey(creds->chain, creds->pkey))) { + md_result_printf(result, rv, "certificate and private key do not match in staged credentials #%d", i); + goto leave; + } + APR_ARRAY_PUSH(all_creds, md_credentials_t*) = creds; + } + /* See if staging holds a new or modified account data */ - rv = md_acme_acct_load(&acct, &acct_key, store, MD_SG_STAGING, name, p); + rv = md_acme_acct_load(&acct, &acct_key, d->store, MD_SG_STAGING, name, d->p); if (APR_STATUS_IS_ENOENT(rv)) { acct = NULL; acct_key = NULL; rv = APR_SUCCESS; } else if (APR_SUCCESS != rv) { - return rv; + md_result_set(result, rv, "loading staged account"); + goto leave; } - /* Remove any authz information we have here or in MD_SG_CHALLENGES */ - md_acme_authz_set_purge(store, MD_SG_STAGING, p, name); + md_result_activity_setn(result, "purging order information"); + md_acme_order_purge(d->store, d->p, MD_SG_STAGING, md, d->env); - 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); + md_result_activity_setn(result, "purging store tmp space"); + rv = md_store_purge(d->store, d->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; + md_result_set(result, rv, NULL); + goto leave; } if (acct) { md_acme_t *acme; + + /* We may have STAGED the same account several times. This happens when + * several MDs are renewed at once and need a new account. They will all store + * the new account in their own STAGING area. By checking for accounts with + * the same url, we save them all into a single one. + */ + md_result_activity_setn(result, "saving staged account"); + id = md->ca_account; + if (!id) { + rv = md_acme_acct_id_for_md(&id, d->store, MD_SG_ACCOUNTS, md, d->p); + if (APR_STATUS_IS_ENOENT(rv)) { + id = NULL; + } + else if (APR_SUCCESS != rv) { + md_result_set(result, rv, "error searching for existing account by url"); + goto leave; + } + } + + if (APR_SUCCESS != (rv = md_acme_create(&acme, d->p, md->ca_effective, + d->proxy_url, d->ca_file))) { + md_result_set(result, rv, "error setting up acme"); + goto leave; + } - 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; + if (APR_SUCCESS != (rv = md_acme_acct_save(d->store, d->p, acme, &id, acct, acct_key))) { + md_result_set(result, rv, "error saving account"); + goto leave; } - md->ca_account = acct->id; - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: saved ACME account %s", - name, acct->id); + md->ca_account = 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; + else if (!md->ca_account) { + /* staging reused another account and did not create a new one. find + * the account, if it is already there */ + rv = md_acme_acct_id_for_md(&id, d->store, MD_SG_ACCOUNTS, md, d->p); + if (APR_SUCCESS == rv) { + md->ca_account = id; + } } - 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; + + md_result_activity_setn(result, "saving staged md/privkey/pubcert"); + if (APR_SUCCESS != (rv = md_save(d->store, d->p, load_group, md, 1))) { + md_result_set(result, rv, "writing md.json"); + goto leave; } - 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; + + for (i = 0; i < all_creds->nelts; ++i) { + creds = APR_ARRAY_IDX(all_creds, i, md_credentials_t*); + if (APR_SUCCESS != (rv = md_creds_save(d->store, d->p, load_group, name, creds, 1))) { + md_result_printf(result, rv, "writing credentials #%d", i); + goto leave; + } } + md_result_set(result, APR_SUCCESS, "saved staged data successfully"); + +leave: + md_result_log(result, MD_LOG_DEBUG); return rv; } -static apr_status_t acme_driver_preload(md_proto_driver_t *d, md_store_group_t group) +static apr_status_t acme_driver_preload(md_proto_driver_t *d, + md_store_group_t group, md_result_t *result) { - 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); + rv = acme_preload(d, group, d->md->name, result); + md_result_log(result, MD_LOG_DEBUG); return rv; } +static apr_status_t acme_complete_md(md_t *md, apr_pool_t *p) +{ + (void)p; + if (!md->ca_urls || apr_is_empty_array(md->ca_urls)) { + md->ca_urls = apr_array_make(p, 3, sizeof(const char *)); + APR_ARRAY_PUSH(md->ca_urls, const char*) = MD_ACME_DEF_URL; + } + return APR_SUCCESS; +} + static md_proto_t ACME_PROTO = { - MD_PROTO_ACME, acme_driver_init, acme_driver_stage, acme_driver_preload + MD_PROTO_ACME, acme_driver_init, acme_driver_renew, + acme_driver_preload_init, acme_driver_preload, + acme_complete_md, }; apr_status_t md_acme_protos_add(apr_hash_t *protos, apr_pool_t *p) diff --git a/modules/md/md_acme_drive.h b/modules/md/md_acme_drive.h new file mode 100644 index 0000000..88761fa --- /dev/null +++ b/modules/md/md_acme_drive.h @@ -0,0 +1,55 @@ +/* Copyright 2019 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed 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_acme_drive_h +#define md_acme_drive_h + +struct apr_array_header_t; +struct md_acme_order_t; +struct md_credentials_t; +struct md_result_t; + +typedef struct md_acme_driver_t { + md_proto_driver_t *driver; + void *sub_driver; + + md_acme_t *acme; + md_t *md; + struct apr_array_header_t *domains; + apr_array_header_t *ca_challenges; + + int complete; + apr_array_header_t *creds; /* the new md_credentials_t */ + + struct md_credentials_t *cred; /* credentials currently being processed */ + const char *chain_up_link; /* Link header "up" from last chain retrieval, + needs to be followed */ + + struct md_acme_order_t *order; + apr_interval_time_t authz_monitor_timeout; + + const char *csr_der_64; + apr_interval_time_t cert_poll_timeout; + +} md_acme_driver_t; + +apr_status_t md_acme_drive_set_acct(struct md_proto_driver_t *d, + struct md_result_t *result); +apr_status_t md_acme_drive_setup_cred_chain(struct md_proto_driver_t *d, + struct md_result_t *result); +apr_status_t md_acme_drive_cert_poll(struct md_proto_driver_t *d, int only_once); + +#endif /* md_acme_drive_h */ + diff --git a/modules/md/md_acme_order.c b/modules/md/md_acme_order.c new file mode 100644 index 0000000..061093a --- /dev/null +++ b/modules/md/md_acme_order.c @@ -0,0 +1,562 @@ +/* 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_result.h" +#include "md_store.h" +#include "md_util.h" + +#include "md_acme.h" +#include "md_acme_authz.h" +#include "md_acme_order.h" + + +md_acme_order_t *md_acme_order_create(apr_pool_t *p) +{ + md_acme_order_t *order; + + order = apr_pcalloc(p, sizeof(*order)); + order->p = p; + order->authz_urls = apr_array_make(p, 5, sizeof(const char *)); + order->challenge_setups = apr_array_make(p, 5, sizeof(const char *)); + + return order; +} + +/**************************************************************************************************/ +/* order conversion */ + +#define MD_KEY_CHALLENGE_SETUPS "challenge-setups" + +static md_acme_order_st order_st_from_str(const char *s) +{ + if (s) { + if (!strcmp("valid", s)) { + return MD_ACME_ORDER_ST_VALID; + } + else if (!strcmp("invalid", s)) { + return MD_ACME_ORDER_ST_INVALID; + } + else if (!strcmp("ready", s)) { + return MD_ACME_ORDER_ST_READY; + } + else if (!strcmp("pending", s)) { + return MD_ACME_ORDER_ST_PENDING; + } + else if (!strcmp("processing", s)) { + return MD_ACME_ORDER_ST_PROCESSING; + } + } + return MD_ACME_ORDER_ST_PENDING; +} + +static const char *order_st_to_str(md_acme_order_st status) +{ + switch (status) { + case MD_ACME_ORDER_ST_PENDING: + return "pending"; + case MD_ACME_ORDER_ST_READY: + return "ready"; + case MD_ACME_ORDER_ST_PROCESSING: + return "processing"; + case MD_ACME_ORDER_ST_VALID: + return "valid"; + case MD_ACME_ORDER_ST_INVALID: + return "invalid"; + default: + return "invalid"; + } +} + +md_json_t *md_acme_order_to_json(md_acme_order_t *order, apr_pool_t *p) +{ + md_json_t *json = md_json_create(p); + + if (order->url) { + md_json_sets(order->url, json, MD_KEY_URL, NULL); + } + md_json_sets(order_st_to_str(order->status), json, MD_KEY_STATUS, NULL); + md_json_setsa(order->authz_urls, json, MD_KEY_AUTHORIZATIONS, NULL); + md_json_setsa(order->challenge_setups, json, MD_KEY_CHALLENGE_SETUPS, NULL); + if (order->finalize) { + md_json_sets(order->finalize, json, MD_KEY_FINALIZE, NULL); + } + if (order->certificate) { + md_json_sets(order->certificate, json, MD_KEY_CERTIFICATE, NULL); + } + return json; +} + +static void order_update_from_json(md_acme_order_t *order, md_json_t *json, apr_pool_t *p) +{ + if (!order->url && md_json_has_key(json, MD_KEY_URL, NULL)) { + order->url = md_json_dups(p, json, MD_KEY_URL, NULL); + } + order->status = order_st_from_str(md_json_gets(json, MD_KEY_STATUS, NULL)); + if (md_json_has_key(json, MD_KEY_AUTHORIZATIONS, NULL)) { + md_json_dupsa(order->authz_urls, p, json, MD_KEY_AUTHORIZATIONS, NULL); + } + if (md_json_has_key(json, MD_KEY_CHALLENGE_SETUPS, NULL)) { + md_json_dupsa(order->challenge_setups, p, json, MD_KEY_CHALLENGE_SETUPS, NULL); + } + if (md_json_has_key(json, MD_KEY_FINALIZE, NULL)) { + order->finalize = md_json_dups(p, json, MD_KEY_FINALIZE, NULL); + } + if (md_json_has_key(json, MD_KEY_CERTIFICATE, NULL)) { + order->certificate = md_json_dups(p, json, MD_KEY_CERTIFICATE, NULL); + } +} + +md_acme_order_t *md_acme_order_from_json(md_json_t *json, apr_pool_t *p) +{ + md_acme_order_t *order = md_acme_order_create(p); + + order_update_from_json(order, json, p); + return order; +} + +apr_status_t md_acme_order_add(md_acme_order_t *order, const char *authz_url) +{ + assert(authz_url); + if (md_array_str_index(order->authz_urls, authz_url, 0, 1) < 0) { + APR_ARRAY_PUSH(order->authz_urls, const char*) = apr_pstrdup(order->p, authz_url); + } + return APR_SUCCESS; +} + +apr_status_t md_acme_order_remove(md_acme_order_t *order, const char *authz_url) +{ + int i; + + assert(authz_url); + i = md_array_str_index(order->authz_urls, authz_url, 0, 1); + if (i >= 0) { + order->authz_urls = md_array_str_remove(order->p, order->authz_urls, authz_url, 1); + return APR_SUCCESS; + } + return APR_ENOENT; +} + +static apr_status_t add_setup_token(md_acme_order_t *order, const char *token) +{ + if (md_array_str_index(order->challenge_setups, token, 0, 1) < 0) { + APR_ARRAY_PUSH(order->challenge_setups, const char*) = apr_pstrdup(order->p, token); + } + return APR_SUCCESS; +} + +/**************************************************************************************************/ +/* persistence */ + +apr_status_t md_acme_order_load(struct md_store_t *store, md_store_group_t group, + const char *md_name, md_acme_order_t **pauthz_set, + apr_pool_t *p) +{ + apr_status_t rv; + md_json_t *json; + md_acme_order_t *authz_set; + + rv = md_store_load_json(store, group, md_name, MD_FN_ORDER, &json, p); + if (APR_SUCCESS == rv) { + authz_set = md_acme_order_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_order_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_order_t *); + create = va_arg(ap, int); + + json = md_acme_order_to_json(set, ptemp); + assert(json); + return md_store_save_json(store, ptemp, group, md_name, MD_FN_ORDER, json, create); +} + +apr_status_t md_acme_order_save(struct md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *md_name, + md_acme_order_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_order_t *order; + md_store_group_t group; + const md_t *md; + const char *setup_token; + apr_table_t *env; + int i; + + group = (md_store_group_t)va_arg(ap, int); + md = va_arg(ap, const md_t *); + env = va_arg(ap, apr_table_t *); + + if (APR_SUCCESS == md_acme_order_load(store, group, md->name, &order, p)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "order loaded for %s", md->name); + for (i = 0; i < order->challenge_setups->nelts; ++i) { + setup_token = APR_ARRAY_IDX(order->challenge_setups, i, const char*); + if (setup_token) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "order teardown setup %s", setup_token); + md_acme_authz_teardown(store, setup_token, md, env, p); + } + } + } + return md_store_remove(store, group, md->name, MD_FN_ORDER, ptemp, 1); +} + +apr_status_t md_acme_order_purge(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const md_t *md, apr_table_t *env) +{ + return md_util_pool_vdo(p_purge, store, p, group, md, env, NULL); +} + +/**************************************************************************************************/ +/* ACMEv2 order requests */ + +typedef struct { + apr_pool_t *p; + md_acme_order_t *order; + md_acme_t *acme; + const char *name; + apr_array_header_t *domains; + md_result_t *result; +} order_ctx_t; + +#define ORDER_CTX_INIT(ctx, p, o, a, n, d, r) \ + (ctx)->p = (p); (ctx)->order = (o); (ctx)->acme = (a); \ + (ctx)->name = (n); (ctx)->domains = d; (ctx)->result = r + +static apr_status_t identifier_to_json(void *value, md_json_t *json, apr_pool_t *p, void *baton) +{ + md_json_t *jid; + + (void)baton; + jid = md_json_create(p); + md_json_sets("dns", jid, "type", NULL); + md_json_sets(value, jid, "value", NULL); + return md_json_setj(jid, json, NULL); +} + +static apr_status_t on_init_order_register(md_acme_req_t *req, void *baton) +{ + order_ctx_t *ctx = baton; + md_json_t *jpayload; + + jpayload = md_json_create(req->p); + md_json_seta(ctx->domains, identifier_to_json, NULL, jpayload, "identifiers", NULL); + + return md_acme_req_body_init(req, jpayload); +} + +static apr_status_t on_order_upd(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, + md_json_t *body, void *baton) +{ + order_ctx_t *ctx = baton; + const char *location = apr_table_get(hdrs, "location"); + apr_status_t rv = APR_SUCCESS; + + (void)acme; + (void)p; + if (!ctx->order) { + if (location) { + ctx->order = md_acme_order_create(ctx->p); + ctx->order->url = apr_pstrdup(ctx->p, location); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ctx->p, "new order at %s", location); + } + else { + rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, ctx->p, "new order, no location header"); + goto out; + } + } + + order_update_from_json(ctx->order, body, ctx->p); +out: + return rv; +} + +apr_status_t md_acme_order_register(md_acme_order_t **porder, md_acme_t *acme, apr_pool_t *p, + const char *name, apr_array_header_t *domains) +{ + order_ctx_t ctx; + apr_status_t rv; + + assert(MD_ACME_VERSION_MAJOR(acme->version) > 1); + ORDER_CTX_INIT(&ctx, p, NULL, acme, name, domains, NULL); + rv = md_acme_POST(acme, acme->api.v2.new_order, on_init_order_register, on_order_upd, NULL, NULL, &ctx); + *porder = (APR_SUCCESS == rv)? ctx.order : NULL; + return rv; +} + +apr_status_t md_acme_order_update(md_acme_order_t *order, md_acme_t *acme, + md_result_t *result, apr_pool_t *p) +{ + order_ctx_t ctx; + apr_status_t rv; + + assert(MD_ACME_VERSION_MAJOR(acme->version) > 1); + ORDER_CTX_INIT(&ctx, p, order, acme, NULL, NULL, result); + rv = md_acme_GET(acme, order->url, NULL, on_order_upd, NULL, NULL, &ctx); + if (APR_SUCCESS != rv && APR_SUCCESS != acme->last->status) { + md_result_dup(result, acme->last); + } + return rv; +} + +static apr_status_t await_ready(void *baton, int attempt) +{ + order_ctx_t *ctx = baton; + apr_status_t rv = APR_SUCCESS; + + (void)attempt; + if (APR_SUCCESS != (rv = md_acme_order_update(ctx->order, ctx->acme, + ctx->result, ctx->p))) goto out; + switch (ctx->order->status) { + case MD_ACME_ORDER_ST_READY: + case MD_ACME_ORDER_ST_PROCESSING: + case MD_ACME_ORDER_ST_VALID: + break; + case MD_ACME_ORDER_ST_PENDING: + rv = APR_EAGAIN; + break; + default: + rv = APR_EINVAL; + break; + } +out: + return rv; +} + +apr_status_t md_acme_order_await_ready(md_acme_order_t *order, md_acme_t *acme, + const md_t *md, apr_interval_time_t timeout, + md_result_t *result, apr_pool_t *p) +{ + order_ctx_t ctx; + apr_status_t rv; + + assert(MD_ACME_VERSION_MAJOR(acme->version) > 1); + ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, result); + + md_result_activity_setn(result, "Waiting for order to become ready"); + rv = md_util_try(await_ready, &ctx, 0, timeout, 0, 0, 1); + md_result_log(result, MD_LOG_DEBUG); + return rv; +} + +static apr_status_t await_valid(void *baton, int attempt) +{ + order_ctx_t *ctx = baton; + apr_status_t rv = APR_SUCCESS; + + (void)attempt; + if (APR_SUCCESS != (rv = md_acme_order_update(ctx->order, ctx->acme, + ctx->result, ctx->p))) goto out; + switch (ctx->order->status) { + case MD_ACME_ORDER_ST_VALID: + md_result_set(ctx->result, APR_EINVAL, "ACME server order status is 'valid'."); + break; + case MD_ACME_ORDER_ST_PROCESSING: + rv = APR_EAGAIN; + break; + case MD_ACME_ORDER_ST_INVALID: + md_result_set(ctx->result, APR_EINVAL, "ACME server order status is 'invalid'."); + rv = APR_EINVAL; + break; + default: + rv = APR_EINVAL; + break; + } +out: + return rv; +} + +apr_status_t md_acme_order_await_valid(md_acme_order_t *order, md_acme_t *acme, + const md_t *md, apr_interval_time_t timeout, + md_result_t *result, apr_pool_t *p) +{ + order_ctx_t ctx; + apr_status_t rv; + + assert(MD_ACME_VERSION_MAJOR(acme->version) > 1); + ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, result); + + md_result_activity_setn(result, "Waiting for finalized order to become valid"); + rv = md_util_try(await_valid, &ctx, 0, timeout, 0, 0, 1); + md_result_log(result, MD_LOG_DEBUG); + return rv; +} + +/**************************************************************************************************/ +/* processing */ + +apr_status_t md_acme_order_start_challenges(md_acme_order_t *order, md_acme_t *acme, + apr_array_header_t *challenge_types, + md_store_t *store, const md_t *md, + apr_table_t *env, md_result_t *result, + apr_pool_t *p) +{ + apr_status_t rv = APR_SUCCESS; + md_acme_authz_t *authz; + const char *url, *setup_token; + int i; + + md_result_activity_printf(result, "Starting challenges for domains"); + for (i = 0; i < order->authz_urls->nelts; ++i) { + url = APR_ARRAY_IDX(order->authz_urls, i, const char*); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: check AUTHZ at %s", md->name, url); + + if (APR_SUCCESS != (rv = md_acme_authz_retrieve(acme, p, url, &authz))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: check authz for %s", + md->name, authz->domain); + goto leave; + } + + switch (authz->state) { + case MD_ACME_AUTHZ_S_VALID: + break; + + case MD_ACME_AUTHZ_S_PENDING: + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "%s: authorization pending for %s", + md->name, authz->domain); + rv = md_acme_authz_respond(authz, acme, store, challenge_types, + md->pks, + md->acme_tls_1_domains, md, + env, p, &setup_token, result); + if (APR_SUCCESS != rv) { + goto leave; + } + add_setup_token(order, setup_token); + md_acme_order_save(store, p, MD_SG_STAGING, md->name, order, 0); + break; + + case MD_ACME_AUTHZ_S_INVALID: + rv = APR_EINVAL; + if (authz->error_type) { + md_result_problem_set(result, rv, authz->error_type, authz->error_detail, NULL); + goto leave; + } + /* fall through */ + default: + rv = APR_EINVAL; + md_result_printf(result, rv, "unexpected AUTHZ state %d for domain %s", + authz->state, authz->domain); + md_result_log(result, MD_LOG_ERR); + goto leave; + } + } +leave: + return rv; +} + +static apr_status_t check_challenges(void *baton, int attempt) +{ + order_ctx_t *ctx = baton; + const char *url; + md_acme_authz_t *authz; + apr_status_t rv = APR_SUCCESS; + int i; + + for (i = 0; i < ctx->order->authz_urls->nelts; ++i) { + url = APR_ARRAY_IDX(ctx->order->authz_urls, i, const char*); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ctx->p, "%s: check AUTHZ at %s (attempt %d)", + ctx->name, url, attempt); + + rv = md_acme_authz_retrieve(ctx->acme, ctx->p, url, &authz); + if (APR_SUCCESS == rv) { + switch (authz->state) { + case MD_ACME_AUTHZ_S_VALID: + md_result_printf(ctx->result, rv, + "domain authorization for %s is valid", authz->domain); + break; + case MD_ACME_AUTHZ_S_PENDING: + rv = APR_EAGAIN; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ctx->p, + "%s: status pending at %s", authz->domain, authz->url); + goto leave; + case MD_ACME_AUTHZ_S_INVALID: + rv = APR_EINVAL; + md_result_printf(ctx->result, rv, + "domain authorization for %s failed, CA considers " + "answer to challenge invalid%s.", + authz->domain, authz->error_type? "" : ", no error given"); + md_result_log(ctx->result, MD_LOG_ERR); + goto leave; + default: + rv = APR_EINVAL; + md_result_printf(ctx->result, rv, + "domain authorization for %s failed with state %d", + authz->domain, authz->state); + md_result_log(ctx->result, MD_LOG_ERR); + goto leave; + } + } + else { + md_result_printf(ctx->result, rv, "authorization retrieval failed for %s on <%s>", + ctx->name, url); + } + } +leave: + return rv; +} + +apr_status_t md_acme_order_monitor_authzs(md_acme_order_t *order, md_acme_t *acme, + const md_t *md, apr_interval_time_t timeout, + md_result_t *result, apr_pool_t *p) +{ + order_ctx_t ctx; + apr_status_t rv; + + ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, result); + + md_result_activity_printf(result, "Monitoring challenge status for %s", md->name); + rv = md_util_try(check_challenges, &ctx, 0, timeout, 0, 0, 1); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: checked authorizations", md->name); + return rv; +} + diff --git a/modules/md/md_acme_order.h b/modules/md/md_acme_order.h new file mode 100644 index 0000000..4170440 --- /dev/null +++ b/modules/md/md_acme_order.h @@ -0,0 +1,91 @@ +/* Copyright 2019 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed 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_acme_order_h +#define md_acme_order_h + +struct md_json_t; +struct md_result_t; + +typedef struct md_acme_order_t md_acme_order_t; + +typedef enum { + MD_ACME_ORDER_ST_PENDING, + MD_ACME_ORDER_ST_READY, + MD_ACME_ORDER_ST_PROCESSING, + MD_ACME_ORDER_ST_VALID, + MD_ACME_ORDER_ST_INVALID, +} md_acme_order_st; + +struct md_acme_order_t { + apr_pool_t *p; + const char *url; + md_acme_order_st status; + struct apr_array_header_t *authz_urls; + struct apr_array_header_t *challenge_setups; + struct md_json_t *json; + const char *finalize; + const char *certificate; +}; + +#define MD_FN_ORDER "order.json" + +/**************************************************************************************************/ + +md_acme_order_t *md_acme_order_create(apr_pool_t *p); + +apr_status_t md_acme_order_add(md_acme_order_t *order, const char *authz_url); +apr_status_t md_acme_order_remove(md_acme_order_t *order, const char *authz_url); + +struct md_json_t *md_acme_order_to_json(md_acme_order_t *set, apr_pool_t *p); +md_acme_order_t *md_acme_order_from_json(struct md_json_t *json, apr_pool_t *p); + +apr_status_t md_acme_order_load(struct md_store_t *store, md_store_group_t group, + const char *md_name, md_acme_order_t **pauthz_set, + apr_pool_t *p); +apr_status_t md_acme_order_save(struct md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *md_name, + md_acme_order_t *authz_set, int create); + +apr_status_t md_acme_order_purge(struct md_store_t *store, apr_pool_t *p, + md_store_group_t group, const md_t *md, + apr_table_t *env); + +apr_status_t md_acme_order_start_challenges(md_acme_order_t *order, md_acme_t *acme, + apr_array_header_t *challenge_types, + md_store_t *store, const md_t *md, + apr_table_t *env, struct md_result_t *result, + apr_pool_t *p); + +apr_status_t md_acme_order_monitor_authzs(md_acme_order_t *order, md_acme_t *acme, + const md_t *md, apr_interval_time_t timeout, + struct md_result_t *result, apr_pool_t *p); + +/* ACMEv2 only ************************************************************************************/ + +apr_status_t md_acme_order_register(md_acme_order_t **porder, md_acme_t *acme, apr_pool_t *p, + const char *name, struct apr_array_header_t *domains); + +apr_status_t md_acme_order_update(md_acme_order_t *order, md_acme_t *acme, + struct md_result_t *result, apr_pool_t *p); + +apr_status_t md_acme_order_await_ready(md_acme_order_t *order, md_acme_t *acme, + const md_t *md, apr_interval_time_t timeout, + struct md_result_t *result, apr_pool_t *p); +apr_status_t md_acme_order_await_valid(md_acme_order_t *order, md_acme_t *acme, + const md_t *md, apr_interval_time_t timeout, + struct md_result_t *result, apr_pool_t *p); + +#endif /* md_acme_order_h */ diff --git a/modules/md/md_acmev2_drive.c b/modules/md/md_acmev2_drive.c new file mode 100644 index 0000000..9dfca96 --- /dev/null +++ b/modules/md/md_acmev2_drive.c @@ -0,0 +1,181 @@ +/* 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_result.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" +#include "md_acme_order.h" + +#include "md_acme_drive.h" +#include "md_acmev2_drive.h" + + + +/**************************************************************************************************/ +/* order setup */ + +/** + * Either we have an order stored in the STAGING area, or we need to create a + * new one at the ACME server. + */ +static apr_status_t ad_setup_order(md_proto_driver_t *d, md_result_t *result, int *pis_new) +{ + md_acme_driver_t *ad = d->baton; + apr_status_t rv; + md_t *md = ad->md; + + assert(ad->md); + assert(ad->acme); + + /* 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 + */ + if (pis_new) *pis_new = 0; + rv = md_acme_order_load(d->store, MD_SG_STAGING, md->name, &ad->order, d->p); + if (APR_SUCCESS == rv) { + md_result_activity_setn(result, "Loaded order from staging"); + goto leave; + } + else if (!APR_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: loading order", md->name); + md_acme_order_purge(d->store, d->p, MD_SG_STAGING, md, d->env); + } + + md_result_activity_setn(result, "Creating new order"); + rv = md_acme_order_register(&ad->order, ad->acme, d->p, d->md->name, ad->domains); + if (APR_SUCCESS !=rv) goto leave; + rv = md_acme_order_save(d->store, d->p, MD_SG_STAGING, d->md->name, ad->order, 0); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, "saving order in staging"); + } + if (pis_new) *pis_new = 1; + +leave: + md_acme_report_result(ad->acme, rv, result); + return rv; +} + +/**************************************************************************************************/ +/* ACMEv2 renewal */ + +apr_status_t md_acmev2_drive_renew(md_acme_driver_t *ad, md_proto_driver_t *d, md_result_t *result) +{ + apr_status_t rv = APR_SUCCESS; + int is_new_order = 0; + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: (ACMEv2) need certificate", d->md->name); + + /* Chose (or create) and ACME account to use */ + rv = md_acme_drive_set_acct(d, result); + if (APR_SUCCESS != rv) goto leave; + + if (!md_array_is_empty(ad->cred->chain)) goto leave; + + /* ACMEv2 strategy: + * 1. load an md_acme_order_t from STAGING, if present + * 2. if no order found, register a new order at ACME server + * 3. update the order from the server + * 4. Switch order state: + * * PENDING: process authz challenges + * * READY: finalize the order + * * PROCESSING: wait and re-assses later + * * VALID: retrieve certificate + * * COMPLETE: all done, return success + * * INVALID and otherwise: fail renewal, delete local order + */ + if (APR_SUCCESS != (rv = ad_setup_order(d, result, &is_new_order))) { + goto leave; + } + + rv = md_acme_order_update(ad->order, ad->acme, result, d->p); + if (APR_STATUS_IS_ENOENT(rv) + || APR_STATUS_IS_EACCES(rv) + || MD_ACME_ORDER_ST_INVALID == ad->order->status) { + /* order is invalid or no longer known at the ACME server */ + ad->order = NULL; + md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env); + } + else if (APR_SUCCESS != rv) { + goto leave; + } + +retry: + if (!ad->order) { + rv = ad_setup_order(d, result, &is_new_order); + if (APR_SUCCESS != rv) goto leave; + } + + rv = md_acme_order_start_challenges(ad->order, ad->acme, ad->ca_challenges, + d->store, d->md, d->env, result, d->p); + if (!is_new_order && APR_STATUS_IS_EINVAL(rv)) { + /* found 'invalid' domains in previous order, need to start over */ + ad->order = NULL; + md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env); + goto retry; + } + if (APR_SUCCESS != rv) goto leave; + + rv = md_acme_order_monitor_authzs(ad->order, ad->acme, d->md, + ad->authz_monitor_timeout, result, d->p); + if (APR_SUCCESS != rv) goto leave; + + rv = md_acme_order_await_ready(ad->order, ad->acme, d->md, + ad->authz_monitor_timeout, result, d->p); + if (APR_SUCCESS != rv) goto leave; + + if (MD_ACME_ORDER_ST_READY == ad->order->status) { + rv = md_acme_drive_setup_cred_chain(d, result); + if (APR_SUCCESS != rv) goto leave; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: finalized order", d->md->name); + } + + rv = md_acme_order_await_valid(ad->order, ad->acme, d->md, + ad->authz_monitor_timeout, result, d->p); + if (APR_SUCCESS != rv) goto leave; + + if (!ad->order->certificate) { + md_result_set(result, APR_EINVAL, "Order valid, but certificate url is missing."); + goto leave; + } + md_result_set(result, APR_SUCCESS, NULL); + +leave: + md_result_log(result, MD_LOG_DEBUG); + return result->status; +} + diff --git a/modules/md/md_acmev2_drive.h b/modules/md/md_acmev2_drive.h new file mode 100644 index 0000000..7552c4f --- /dev/null +++ b/modules/md/md_acmev2_drive.h @@ -0,0 +1,27 @@ +/* Copyright 2019 greenbytes GmbH (https://www.greenbytes.de) + * + * Licensed 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_acmev2_drive_h +#define md_acmev2_drive_h + +struct md_json_t; +struct md_proto_driver_t; +struct md_result_t; + +apr_status_t md_acmev2_drive_renew(struct md_acme_driver_t *ad, + struct md_proto_driver_t *d, + struct md_result_t *result); + +#endif /* md_acmev2_drive_h */ diff --git a/modules/md/md_core.c b/modules/md/md_core.c index 51ad005..7aacff0 100644 --- a/modules/md/md_core.c +++ b/modules/md/md_core.c @@ -19,6 +19,7 @@ #include <apr_lib.h> #include <apr_strings.h> +#include <apr_uri.h> #include <apr_tables.h> #include <apr_time.h> #include <apr_date.h> @@ -33,7 +34,10 @@ 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; + if (md_array_str_index(md->domains, domain, 0, case_sensitive) >= 0) { + return 1; + } + return md_dns_domains_match(md->domains, domain); } const char *md_common_name(const md_t *md1, const md_t *md2) @@ -79,16 +83,35 @@ apr_size_t md_common_name_count(const md_t *md1, const md_t *md2) return hits; } +int md_is_covered_by_alt_names(const md_t *md, const struct apr_array_header_t* alt_names) +{ + const char *name; + int i; + + if (alt_names) { + for (i = 0; i < md->domains->nelts; ++i) { + name = APR_ARRAY_IDX(md->domains, i, const char *); + if (!md_dns_domains_match(alt_names, name)) { + return 0; + } + } + return 1; + } + return 0; +} + 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->renew_mode = MD_RENEW_DEFAULT; md->require_https = MD_REQUIRE_UNSET; md->must_staple = -1; md->transitive = -1; + md->acme_tls_1_domains = apr_array_make(p, 5, sizeof(const char *)); + md->stapling = -1; md->defn_name = "unknown"; md->defn_line_number = 0; } @@ -125,38 +148,6 @@ int md_contains_domains(const md_t *md1, const md_t *md2) 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; @@ -193,6 +184,15 @@ md_t *md_get_by_dns_overlap(struct apr_array_header_t *mds, const md_t *md) return NULL; } +int md_cert_count(const md_t *md) +{ + /* cert are defined as a list of static files or a list of private key specs */ + if (md->cert_files && md->cert_files->nelts) { + return md->cert_files->nelts; + } + return md_pkeys_spec_count(md->pks); +} + md_t *md_create(apr_pool_t *p, apr_array_header_t *domains) { md_t *md; @@ -204,34 +204,6 @@ md_t *md_create(apr_pool_t *p, apr_array_header_t *domains) 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 */ @@ -247,6 +219,8 @@ md_t *md_copy(apr_pool_t *p, const md_t *src) if (src->ca_challenges) { md->ca_challenges = apr_array_copy(p, src->ca_challenges); } + md->acme_tls_1_domains = apr_array_copy(p, src->acme_tls_1_domains); + md->pks = md_pkeys_spec_clone(p, src->pks); } return md; } @@ -261,49 +235,33 @@ md_t *md_clone(apr_pool_t *p, const md_t *src) 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->renew_mode = src->renew_mode; md->domains = md_array_str_compact(p, src->domains, 0); - md->pkey_spec = src->pkey_spec; - md->renew_norm = src->renew_norm; + md->pks = md_pkeys_spec_clone(p, src->pks); md->renew_window = src->renew_window; + md->warn_window = src->warn_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_urls) { + md->ca_urls = md_array_str_clone(p, src->ca_urls); + } + if (src->ca_effective) md->ca_effective = apr_pstrdup(p, src->ca_effective); 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); } + md->acme_tls_1_domains = md_array_str_compact(p, src->acme_tls_1_domains, 0); + md->stapling = src->stapling; + if (src->dns01_cmd) md->dns01_cmd = apr_pstrdup(p, src->dns01_cmd); + if (src->cert_files) md->cert_files = md_array_str_clone(p, src->cert_files); + if (src->pkey_files) md->pkey_files = md_array_str_clone(p, src->pkey_files); } 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 */ @@ -318,34 +276,22 @@ md_json_t *md_to_json(const md_t *md, apr_pool_t *p) 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); + md_json_sets(md->ca_effective, json, MD_KEY_CA, MD_KEY_URL, NULL); + if (md->ca_urls && !apr_is_empty_array(md->ca_urls)) { + md_json_setsa(md->ca_urls, json, MD_KEY_CA, MD_KEY_URLS, NULL); } - if (md->pkey_spec) { - md_json_setj(md_pkey_spec_to_json(md->pkey_spec, p), json, MD_KEY_PKEY, NULL); + md_json_sets(md->ca_agreement, json, MD_KEY_CA, MD_KEY_AGREEMENT, NULL); + if (!md_pkeys_spec_is_empty(md->pks)) { + md_json_setj(md_pkeys_spec_to_json(md->pks, 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->state_descr) + md_json_sets(md->state_descr, json, MD_KEY_STATE_DESCR, NULL); + md_json_setl(md->renew_mode, json, MD_KEY_RENEW_MODE, NULL); + if (md->renew_window) + md_json_sets(md_timeslice_format(md->renew_window, p), json, MD_KEY_RENEW_WINDOW, NULL); + if (md->warn_window) + md_json_sets(md_timeslice_format(md->warn_window, p), json, MD_KEY_WARN_WINDOW, 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); @@ -362,6 +308,15 @@ md_json_t *md_to_json(const md_t *md, apr_pool_t *p) break; } md_json_setb(md->must_staple > 0, json, MD_KEY_MUST_STAPLE, NULL); + md_json_setsa(md->acme_tls_1_domains, json, MD_KEY_PROTO, MD_KEY_ACME_TLS_1, NULL); + if (md->cert_files) md_json_setsa(md->cert_files, json, MD_KEY_CERT_FILES, NULL); + if (md->pkey_files) md_json_setsa(md->pkey_files, json, MD_KEY_PKEY_FILES, NULL); + md_json_setb(md->stapling > 0, json, MD_KEY_STAPLING, NULL); + if (md->dns01_cmd) md_json_sets(md->dns01_cmd, json, MD_KEY_CMD_DNS01, NULL); + if (md->ca_eab_kid && strcmp("none", md->ca_eab_kid)) { + md_json_sets(md->ca_eab_kid, json, MD_KEY_EAB, MD_KEY_KID, NULL); + if (md->ca_eab_hmac) md_json_sets(md->ca_eab_hmac, json, MD_KEY_EAB, MD_KEY_HMAC, NULL); + } return json; } return NULL; @@ -377,36 +332,30 @@ md_t *md_from_json(md_json_t *json, apr_pool_t *p) 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_effective = md_json_dups(p, json, MD_KEY_CA, MD_KEY_URL, NULL); + if (md_json_has_key(json, MD_KEY_CA, MD_KEY_URLS, NULL)) { + md->ca_urls = apr_array_make(p, 5, sizeof(const char*)); + md_json_dupsa(md->ca_urls, p, json, MD_KEY_CA, MD_KEY_URLS, NULL); + } + else if (md->ca_effective) { + /* compat for old format where we had only a single url */ + md->ca_urls = apr_array_make(p, 5, sizeof(const char*)); + APR_ARRAY_PUSH(md->ca_urls, const char*) = md->ca_effective; + } 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); + if (md_json_has_key(json, MD_KEY_PKEY, NULL)) { + md->pks = md_pkeys_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->state_descr = md_json_dups(p, json, MD_KEY_STATE_DESCR, NULL); + if (MD_S_EXPIRED_DEPRECATED == md->state) md->state = MD_S_COMPLETE; + md->renew_mode = (int)md_json_getl(json, MD_KEY_RENEW_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); - } - } - } + s = md_json_gets(json, MD_KEY_RENEW_WINDOW, NULL); + md_timeslice_parse(&md->renew_window, p, s, MD_TIME_LIFE_NORM); + s = md_json_gets(json, MD_KEY_WARN_WINDOW, NULL); + md_timeslice_parse(&md->warn_window, p, s, MD_TIME_LIFE_NORM); 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); @@ -420,9 +369,94 @@ md_t *md_from_json(md_json_t *json, apr_pool_t *p) md->require_https = MD_REQUIRE_PERMANENT; } md->must_staple = (int)md_json_getb(json, MD_KEY_MUST_STAPLE, NULL); - + md_json_dupsa(md->acme_tls_1_domains, p, json, MD_KEY_PROTO, MD_KEY_ACME_TLS_1, NULL); + + if (md_json_has_key(json, MD_KEY_CERT_FILES, NULL)) { + md->cert_files = apr_array_make(p, 3, sizeof(char*)); + md->pkey_files = apr_array_make(p, 3, sizeof(char*)); + md_json_dupsa(md->cert_files, p, json, MD_KEY_CERT_FILES, NULL); + md_json_dupsa(md->pkey_files, p, json, MD_KEY_PKEY_FILES, NULL); + } + md->stapling = (int)md_json_getb(json, MD_KEY_STAPLING, NULL); + md->dns01_cmd = md_json_dups(p, json, MD_KEY_CMD_DNS01, NULL); + if (md_json_has_key(json, MD_KEY_EAB, NULL)) { + md->ca_eab_kid = md_json_dups(p, json, MD_KEY_EAB, MD_KEY_KID, NULL); + md->ca_eab_hmac = md_json_dups(p, json, MD_KEY_EAB, MD_KEY_HMAC, NULL); + } return md; } return NULL; } +md_json_t *md_to_public_json(const md_t *md, apr_pool_t *p) +{ + md_json_t *json = md_to_json(md, p); + if (md_json_has_key(json, MD_KEY_EAB, MD_KEY_HMAC, NULL)) { + md_json_sets("***", json, MD_KEY_EAB, MD_KEY_HMAC, NULL); + } + return json; +} + +typedef struct { + const char *name; + const char *url; +} md_ca_t; + +#define LE_ACMEv2_PROD "https://acme-v02.api.letsencrypt.org/directory" +#define LE_ACMEv2_STAGING "https://acme-staging-v02.api.letsencrypt.org/directory" +#define BUYPASS_ACME "https://api.buypass.com/acme/directory" +#define BUYPASS_ACME_TEST "https://api.test4.buypass.no/acme/directory" + +static md_ca_t KNOWN_CAs[] = { + { "LetsEncrypt", LE_ACMEv2_PROD }, + { "LetsEncrypt-Test", LE_ACMEv2_STAGING }, + { "Buypass", BUYPASS_ACME }, + { "Buypass-Test", BUYPASS_ACME_TEST }, +}; + +const char *md_get_ca_name_from_url(apr_pool_t *p, const char *url) +{ + apr_uri_t uri_parsed; + unsigned int i; + + for (i = 0; i < sizeof(KNOWN_CAs)/sizeof(KNOWN_CAs[0]); ++i) { + if (!apr_strnatcasecmp(KNOWN_CAs[i].url, url)) { + return KNOWN_CAs[i].name; + } + } + if (APR_SUCCESS == apr_uri_parse(p, url, &uri_parsed)) { + return uri_parsed.hostname; + } + return apr_pstrdup(p, url); +} + +apr_status_t md_get_ca_url_from_name(const char **purl, apr_pool_t *p, const char *name) +{ + const char *err; + unsigned int i; + apr_status_t rv = APR_SUCCESS; + + *purl = NULL; + for (i = 0; i < sizeof(KNOWN_CAs)/sizeof(KNOWN_CAs[0]); ++i) { + if (!apr_strnatcasecmp(KNOWN_CAs[i].name, name)) { + *purl = KNOWN_CAs[i].url; + goto leave; + } + } + *purl = name; + rv = md_util_abs_uri_check(p, name, &err); + if (APR_SUCCESS != rv) { + apr_array_header_t *names; + + names = apr_array_make(p, 10, sizeof(const char*)); + for (i = 0; i < sizeof(KNOWN_CAs)/sizeof(KNOWN_CAs[0]); ++i) { + APR_ARRAY_PUSH(names, const char *) = KNOWN_CAs[i].name; + } + *purl = apr_psprintf(p, + "The CA name '%s' is not known and it is not a URL either (%s). " + "Known CA names are: %s.", + name, err, apr_array_pstrcat(p, names, ' ')); + } +leave: + return rv; +} diff --git a/modules/md/md_crypt.c b/modules/md/md_crypt.c index e0aac3e..4b2af89 100644 --- a/modules/md/md_crypt.c +++ b/modules/md/md_crypt.c @@ -22,19 +22,26 @@ #include <apr_buckets.h> #include <apr_file_io.h> #include <apr_strings.h> +#include <httpd.h> +#include <http_core.h> #include <openssl/err.h> #include <openssl/evp.h> +#include <openssl/hmac.h> #include <openssl/pem.h> #include <openssl/rand.h> #include <openssl/rsa.h> #include <openssl/x509v3.h> +#if OPENSSL_VERSION_NUMBER >= 0x30000000L +#include <openssl/core_names.h> +#endif #include "md.h" #include "md_crypt.h" #include "md_json.h" #include "md_log.h" #include "md_http.h" +#include "md_time.h" #include "md_util.h" /* getpid for *NIX */ @@ -57,6 +64,17 @@ #define MD_USE_OPENSSL_PRE_1_1_API (OPENSSL_VERSION_NUMBER < 0x10100000L) #endif +#if (defined(LIBRESSL_VERSION_NUMBER) && (LIBRESSL_VERSION_NUMBER < 0x3050000fL)) || (OPENSSL_VERSION_NUMBER < 0x10100000L) +/* Missing from LibreSSL < 3.5.0 and only available since OpenSSL v1.1.x */ +#ifndef OPENSSL_NO_CT +#define OPENSSL_NO_CT +#endif +#endif + +#ifndef OPENSSL_NO_CT +#include <openssl/ct.h> +#endif + static int initialized; struct md_pkey_t { @@ -142,17 +160,13 @@ apr_status_t md_crypt_init(apr_pool_t *pool) 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; + md_data_t *buf = baton; + apr_size_t wlen; (void)p; - return apr_file_write_full(f, buf->data, buf->len, &buf->len); + return apr_file_write_full(f, buf->data, buf->len, &wlen); } apr_status_t md_rand_bytes(unsigned char *buf, apr_size_t len, apr_pool_t *p) @@ -183,8 +197,10 @@ static int pem_passwd(char *buf, int size, int rwflag, void *baton) size = (int)ctx->pass_len; } memcpy(buf, ctx->pass_phrase, (size_t)size); + } else { + return 0; } - return ctx->pass_len; + return size; } /**************************************************************************************************/ @@ -197,7 +213,8 @@ static int pem_passwd(char *buf, int size, int rwflag, void *baton) */ static apr_time_t md_asn1_time_get(const ASN1_TIME* time) { -#if OPENSSL_VERSION_NUMBER < 0x10002000L || defined(LIBRESSL_VERSION_NUMBER) +#if OPENSSL_VERSION_NUMBER < 0x10002000L || (defined(LIBRESSL_VERSION_NUMBER) && \ + LIBRESSL_VERSION_NUMBER < 0x3060000fL) /* courtesy: https://stackoverflow.com/questions/10975542/asn1-time-to-time-t-conversion#11263731 * all bugs are mine */ apr_time_exp_t t; @@ -246,10 +263,98 @@ static apr_time_t md_asn1_time_get(const ASN1_TIME* time) #endif } +apr_time_t md_asn1_generalized_time_get(void *ASN1_GENERALIZEDTIME) +{ + return md_asn1_time_get(ASN1_GENERALIZEDTIME); +} + +/**************************************************************************************************/ +/* OID/NID things */ + +static int get_nid(const char *num, const char *sname, const char *lname) +{ + /* Funny API, an OID for a feature might be configured or + * maybe not. In the second case, we need to add it. But adding + * when it already is there is an error... */ + int nid = OBJ_txt2nid(num); + if (NID_undef == nid) { + nid = OBJ_create(num, sname, lname); + } + return nid; +} + +#define MD_GET_NID(x) get_nid(MD_OID_##x##_NUM, MD_OID_##x##_SNAME, MD_OID_##x##_LNAME) /**************************************************************************************************/ /* private keys */ +md_pkeys_spec_t *md_pkeys_spec_make(apr_pool_t *p) +{ + md_pkeys_spec_t *pks; + + pks = apr_pcalloc(p, sizeof(*pks)); + pks->p = p; + pks->specs = apr_array_make(p, 2, sizeof(md_pkey_spec_t*)); + return pks; +} + +void md_pkeys_spec_add(md_pkeys_spec_t *pks, md_pkey_spec_t *spec) +{ + APR_ARRAY_PUSH(pks->specs, md_pkey_spec_t*) = spec; +} + +void md_pkeys_spec_add_default(md_pkeys_spec_t *pks) +{ + md_pkey_spec_t *spec; + + spec = apr_pcalloc(pks->p, sizeof(*spec)); + spec->type = MD_PKEY_TYPE_DEFAULT; + md_pkeys_spec_add(pks, spec); +} + +int md_pkeys_spec_contains_rsa(md_pkeys_spec_t *pks) +{ + md_pkey_spec_t *spec; + int i; + for (i = 0; i < pks->specs->nelts; ++i) { + spec = APR_ARRAY_IDX(pks->specs, i, md_pkey_spec_t*); + if (MD_PKEY_TYPE_RSA == spec->type) return 1; + } + return 0; +} + +void md_pkeys_spec_add_rsa(md_pkeys_spec_t *pks, unsigned int bits) +{ + md_pkey_spec_t *spec; + + spec = apr_pcalloc(pks->p, sizeof(*spec)); + spec->type = MD_PKEY_TYPE_RSA; + spec->params.rsa.bits = bits; + md_pkeys_spec_add(pks, spec); +} + +int md_pkeys_spec_contains_ec(md_pkeys_spec_t *pks, const char *curve) +{ + md_pkey_spec_t *spec; + int i; + for (i = 0; i < pks->specs->nelts; ++i) { + spec = APR_ARRAY_IDX(pks->specs, i, md_pkey_spec_t*); + if (MD_PKEY_TYPE_EC == spec->type + && !apr_strnatcasecmp(curve, spec->params.ec.curve)) return 1; + } + return 0; +} + +void md_pkeys_spec_add_ec(md_pkeys_spec_t *pks, const char *curve) +{ + md_pkey_spec_t *spec; + + spec = apr_pcalloc(pks->p, sizeof(*spec)); + spec->type = MD_PKEY_TYPE_EC; + spec->params.ec.curve = apr_pstrdup(pks->p, curve); + md_pkeys_spec_add(pks, spec); +} + 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); @@ -264,6 +369,12 @@ md_json_t *md_pkey_spec_to_json(const md_pkey_spec_t *spec, apr_pool_t *p) md_json_setl((long)spec->params.rsa.bits, json, MD_KEY_BITS, NULL); } break; + case MD_PKEY_TYPE_EC: + md_json_sets("EC", json, MD_KEY_TYPE, NULL); + if (spec->params.ec.curve) { + md_json_sets(spec->params.ec.curve, json, MD_KEY_CURVE, NULL); + } + break; default: md_json_sets("Unsupported", json, MD_KEY_TYPE, NULL); break; @@ -272,6 +383,27 @@ md_json_t *md_pkey_spec_to_json(const md_pkey_spec_t *spec, apr_pool_t *p) return json; } +static apr_status_t spec_to_json(void *value, md_json_t *json, apr_pool_t *p, void *baton) +{ + md_json_t *jspec; + + (void)baton; + jspec = md_pkey_spec_to_json((md_pkey_spec_t*)value, p); + return md_json_setj(jspec, json, NULL); +} + +md_json_t *md_pkeys_spec_to_json(const md_pkeys_spec_t *pks, apr_pool_t *p) +{ + md_json_t *j; + + if (pks->specs->nelts == 1) { + return md_pkey_spec_to_json(md_pkeys_spec_get(pks, 0), p); + } + j = md_json_create(p); + md_json_seta(pks->specs, spec_to_json, (void*)pks, j, "specs", NULL); + return md_json_getj(j, "specs", NULL); +} + 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)); @@ -293,29 +425,161 @@ md_pkey_spec_t *md_pkey_spec_from_json(struct md_json_t *json, apr_pool_t *p) spec->params.rsa.bits = MD_PKEY_RSA_BITS_DEF; } } + else if (!apr_strnatcasecmp("EC", s)) { + spec->type = MD_PKEY_TYPE_EC; + s = md_json_gets(json, MD_KEY_CURVE, NULL); + if (s) { + spec->params.ec.curve = apr_pstrdup(p, s); + } + else { + spec->params.ec.curve = NULL; + } + } } return spec; } -int md_pkey_spec_eq(md_pkey_spec_t *spec1, md_pkey_spec_t *spec2) +static apr_status_t spec_from_json(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton) +{ + (void)baton; + *pvalue = md_pkey_spec_from_json(json, p); + return APR_SUCCESS; +} + +md_pkeys_spec_t *md_pkeys_spec_from_json(struct md_json_t *json, apr_pool_t *p) +{ + md_pkeys_spec_t *pks; + md_pkey_spec_t *spec; + + pks = md_pkeys_spec_make(p); + if (md_json_is(MD_JSON_TYPE_ARRAY, json, NULL)) { + md_json_geta(pks->specs, spec_from_json, pks, json, NULL); + } + else { + spec = md_pkey_spec_from_json(json, p); + md_pkeys_spec_add(pks, spec); + } + return pks; +} + +static int pkey_spec_eq(md_pkey_spec_t *s1, md_pkey_spec_t *s2) { - if (spec1 == spec2) { + if (s1 == s2) { return 1; } - if (spec1 && spec2 && spec1->type == spec2->type) { - switch (spec1->type) { + if (s1 && s2 && s1->type == s2->type) { + switch (s1->type) { case MD_PKEY_TYPE_DEFAULT: return 1; case MD_PKEY_TYPE_RSA: - if (spec1->params.rsa.bits == spec2->params.rsa.bits) { + if (s1->params.rsa.bits == s2->params.rsa.bits) { return 1; } break; + case MD_PKEY_TYPE_EC: + if (s1->params.ec.curve == s2->params.ec.curve) { + return 1; + } + else if (!s1->params.ec.curve || !s2->params.ec.curve) { + return 0; + } + return !strcmp(s1->params.ec.curve, s2->params.ec.curve); } } return 0; } +int md_pkeys_spec_eq(md_pkeys_spec_t *pks1, md_pkeys_spec_t *pks2) +{ + int i; + + if (pks1 == pks2) { + return 1; + } + if (pks1 && pks2 && pks1->specs->nelts == pks2->specs->nelts) { + for(i = 0; i < pks1->specs->nelts; ++i) { + if (!pkey_spec_eq(APR_ARRAY_IDX(pks1->specs, i, md_pkey_spec_t *), + APR_ARRAY_IDX(pks2->specs, i, md_pkey_spec_t *))) { + return 0; + } + } + return 1; + } + return 0; +} + +static md_pkey_spec_t *pkey_spec_clone(apr_pool_t *p, md_pkey_spec_t *spec) +{ + md_pkey_spec_t *nspec; + + nspec = apr_pcalloc(p, sizeof(*nspec)); + nspec->type = spec->type; + switch (spec->type) { + case MD_PKEY_TYPE_DEFAULT: + break; + case MD_PKEY_TYPE_RSA: + nspec->params.rsa.bits = spec->params.rsa.bits; + break; + case MD_PKEY_TYPE_EC: + nspec->params.ec.curve = apr_pstrdup(p, spec->params.ec.curve); + break; + } + return nspec; +} + +const char *md_pkey_spec_name(const md_pkey_spec_t *spec) +{ + if (!spec) return "rsa"; + switch (spec->type) { + case MD_PKEY_TYPE_DEFAULT: + case MD_PKEY_TYPE_RSA: + return "rsa"; + case MD_PKEY_TYPE_EC: + return spec->params.ec.curve; + } + return "unknown"; +} + +int md_pkeys_spec_is_empty(const md_pkeys_spec_t *pks) +{ + return NULL == pks || 0 == pks->specs->nelts; +} + +md_pkeys_spec_t *md_pkeys_spec_clone(apr_pool_t *p, const md_pkeys_spec_t *pks) +{ + md_pkeys_spec_t *npks = NULL; + md_pkey_spec_t *spec; + int i; + + if (pks && pks->specs->nelts > 0) { + npks = apr_pcalloc(p, sizeof(*npks)); + npks->specs = apr_array_make(p, pks->specs->nelts, sizeof(md_pkey_spec_t*)); + for (i = 0; i < pks->specs->nelts; ++i) { + spec = APR_ARRAY_IDX(pks->specs, i, md_pkey_spec_t*); + APR_ARRAY_PUSH(npks->specs, md_pkey_spec_t*) = pkey_spec_clone(p, spec); + } + } + return npks; +} + +int md_pkeys_spec_count(const md_pkeys_spec_t *pks) +{ + return md_pkeys_spec_is_empty(pks)? 1 : pks->specs->nelts; +} + +static md_pkey_spec_t PkeySpecDef = { MD_PKEY_TYPE_DEFAULT, {{ 0 }} }; + +md_pkey_spec_t *md_pkeys_spec_get(const md_pkeys_spec_t *pks, int index) +{ + if (md_pkeys_spec_is_empty(pks)) { + return index == 1? &PkeySpecDef : NULL; + } + else if (pks && index >= 0 && index < pks->specs->nelts) { + return APR_ARRAY_IDX(pks->specs, index, md_pkey_spec_t*); + } + return NULL; +} + static md_pkey_t *make_pkey(apr_pool_t *p) { md_pkey_t *pkey = apr_pcalloc(p, sizeof(*pkey)); @@ -377,13 +641,14 @@ apr_status_t md_pkey_fload(md_pkey_t **ppkey, apr_pool_t *p, return rv; } -static apr_status_t pkey_to_buffer(buffer_rec *buffer, md_pkey_t *pkey, apr_pool_t *p, +static apr_status_t pkey_to_buffer(md_data_t *buf, 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; + apr_status_t rv = APR_SUCCESS; passwd_ctx ctx; unsigned long err; int i; @@ -392,7 +657,8 @@ static apr_status_t pkey_to_buffer(buffer_rec *buffer, md_pkey_t *pkey, apr_pool return APR_ENOMEM; } if (pass_len > INT_MAX) { - return APR_EINVAL; + rv = APR_EINVAL; + goto cleanup; } if (pass && pass_len > 0) { ctx.pass_phrase = pass; @@ -401,35 +667,42 @@ static apr_status_t pkey_to_buffer(buffer_rec *buffer, md_pkey_t *pkey, apr_pool cb_baton = &ctx; cipher = EVP_aes_256_cbc(); if (!cipher) { - return APR_ENOTIMPL; + rv = APR_ENOTIMPL; + goto cleanup; } } ERR_clear_error(); +#if 1 + if (!PEM_write_bio_PKCS8PrivateKey(bio, pkey->pkey, cipher, NULL, 0, cb, cb_baton)) { +#else if (!PEM_write_bio_PrivateKey(bio, pkey->pkey, cipher, NULL, 0, cb, cb_baton)) { - BIO_free(bio); +#endif 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; + rv = APR_EINVAL; + goto cleanup; } + md_data_null(buf); 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; + buf->data = apr_palloc(p, (apr_size_t)i); + i = BIO_read(bio, (char*)buf->data, i); + buf->len = (apr_size_t)i; } + +cleanup: BIO_free(bio); - return APR_SUCCESS; + return rv; } 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; + md_data_t buffer; apr_status_t rv; if (APR_SUCCESS == (rv = pkey_to_buffer(&buffer, pkey, p, pass_phrase, pass_len))) { @@ -440,6 +713,71 @@ apr_status_t md_pkey_fsave(md_pkey_t *pkey, apr_pool_t *p, return rv; } +apr_status_t md_pkey_read_http(md_pkey_t **ppkey, apr_pool_t *pool, + const struct md_http_response_t *res) +{ + apr_status_t rv; + apr_off_t data_len; + char *pem_data; + apr_size_t pem_len; + md_pkey_t *pkey; + BIO *bf; + passwd_ctx ctx; + + rv = apr_brigade_length(res->body, 1, &data_len); + if (APR_SUCCESS != rv) goto leave; + if (data_len > 1024*1024) { /* certs usually are <2k each */ + rv = APR_EINVAL; + goto leave; + } + rv = apr_brigade_pflatten(res->body, &pem_data, &pem_len, res->req->pool); + if (APR_SUCCESS != rv) goto leave; + + if (NULL == (bf = BIO_new_mem_buf(pem_data, (int)pem_len))) { + rv = APR_ENOMEM; + goto leave; + } + pkey = make_pkey(pool); + ctx.pass_phrase = NULL; + ctx.pass_len = 0; + ERR_clear_error(); + pkey->pkey = PEM_read_bio_PrivateKey(bf, NULL, NULL, &ctx); + BIO_free(bf); + + if (pkey->pkey == NULL) { + unsigned long err = ERR_get_error(); + rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, pool, + "error loading pkey from http response: %s", + ERR_error_string(err, NULL)); + goto leave; + } + rv = APR_SUCCESS; + apr_pool_cleanup_register(pool, pkey, pkey_cleanup, apr_pool_cleanup_null); + +leave: + *ppkey = (APR_SUCCESS == rv)? pkey : NULL; + return rv; +} + +/* Determine the message digest used for signing with the given private key. + */ +static const EVP_MD *pkey_get_MD(md_pkey_t *pkey) +{ + switch (EVP_PKEY_id(pkey->pkey)) { +#ifdef NID_ED25519 + case NID_ED25519: + return NULL; +#endif +#ifdef NID_ED448 + case NID_ED448: + return NULL; +#endif + default: + return EVP_sha256(); + } +} + static apr_status_t gen_rsa(md_pkey_t **ppkey, apr_pool_t *p, unsigned int bits) { EVP_PKEY_CTX *ctx = NULL; @@ -465,6 +803,143 @@ static apr_status_t gen_rsa(md_pkey_t **ppkey, apr_pool_t *p, unsigned int bits) return rv; } +static apr_status_t check_EC_curve(int nid, apr_pool_t *p) { + EC_builtin_curve *curves = NULL; + size_t nc, i; + int rv = APR_ENOENT; + + nc = EC_get_builtin_curves(NULL, 0); + if (NULL == (curves = OPENSSL_malloc(sizeof(*curves) * nc)) || + nc != EC_get_builtin_curves(curves, nc)) { + rv = APR_EGENERAL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, + "error looking up OpenSSL builtin EC curves"); + goto leave; + } + for (i = 0; i < nc; ++i) { + if (nid == curves[i].nid) { + rv = APR_SUCCESS; + break; + } + } +leave: + OPENSSL_free(curves); + return rv; +} + +static apr_status_t gen_ec(md_pkey_t **ppkey, apr_pool_t *p, const char *curve) +{ + EVP_PKEY_CTX *ctx = NULL; + apr_status_t rv; + int curve_nid = NID_undef; + + /* 1. Convert the cure into its registered identifier. Curves can be known under + * different names. + * 2. Determine, if the curve is supported by OpenSSL (or whatever is linked). + * 3. Generate the key, respecting the specific quirks some curves require. + */ + curve_nid = EC_curve_nist2nid(curve); + /* In case this fails, try some names from other standards, like SECG */ +#ifdef NID_secp384r1 + if (NID_undef == curve_nid && !apr_strnatcasecmp("secp384r1", curve)) { + curve_nid = NID_secp384r1; + curve = EC_curve_nid2nist(curve_nid); + } +#endif +#ifdef NID_X9_62_prime256v1 + if (NID_undef == curve_nid && !apr_strnatcasecmp("secp256r1", curve)) { + curve_nid = NID_X9_62_prime256v1; + curve = EC_curve_nid2nist(curve_nid); + } +#endif +#ifdef NID_X9_62_prime192v1 + if (NID_undef == curve_nid && !apr_strnatcasecmp("secp192r1", curve)) { + curve_nid = NID_X9_62_prime192v1; + curve = EC_curve_nid2nist(curve_nid); + } +#endif +#if defined(NID_X25519) && (!defined(LIBRESSL_VERSION_NUMBER) || \ + LIBRESSL_VERSION_NUMBER >= 0x3070000fL) + if (NID_undef == curve_nid && !apr_strnatcasecmp("X25519", curve)) { + curve_nid = NID_X25519; + curve = EC_curve_nid2nist(curve_nid); + } +#endif + if (NID_undef == curve_nid) { + /* OpenSSL object/curve names */ + curve_nid = OBJ_sn2nid(curve); + } + if (NID_undef == curve_nid) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "ec curve unknown: %s", curve); + rv = APR_ENOTIMPL; goto leave; + } + + *ppkey = make_pkey(p); + switch (curve_nid) { + +#if defined(NID_X25519) && (!defined(LIBRESSL_VERSION_NUMBER) || \ + LIBRESSL_VERSION_NUMBER >= 0x3070000fL) + case NID_X25519: + /* no parameters */ + if (NULL == (ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL)) + || EVP_PKEY_keygen_init(ctx) <= 0 + || EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) <= 0) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, + "error generate EC key for group: %s", curve); + rv = APR_EGENERAL; goto leave; + } + rv = APR_SUCCESS; + break; +#endif + +#if defined(NID_X448) && !defined(LIBRESSL_VERSION_NUMBER) + case NID_X448: + /* no parameters */ + if (NULL == (ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X448, NULL)) + || EVP_PKEY_keygen_init(ctx) <= 0 + || EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) <= 0) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, + "error generate EC key for group: %s", curve); + rv = APR_EGENERAL; goto leave; + } + rv = APR_SUCCESS; + break; +#endif + + default: +#if OPENSSL_VERSION_NUMBER < 0x30000000L + if (APR_SUCCESS != (rv = check_EC_curve(curve_nid, p))) goto leave; + if (NULL == (ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL)) + || EVP_PKEY_paramgen_init(ctx) <= 0 + || EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, curve_nid) <= 0 + || EVP_PKEY_CTX_set_ec_param_enc(ctx, OPENSSL_EC_NAMED_CURVE) <= 0 + || EVP_PKEY_keygen_init(ctx) <= 0 + || EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) <= 0) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, + "error generate EC key for group: %s", curve); + rv = APR_EGENERAL; goto leave; + } +#else + if (APR_SUCCESS != (rv = check_EC_curve(curve_nid, p))) goto leave; + if (NULL == (ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL)) + || EVP_PKEY_keygen_init(ctx) <= 0 + || EVP_PKEY_CTX_ctrl_str(ctx, "ec_paramgen_curve", curve) <= 0 + || EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) <= 0) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, + "error generate EC key for group: %s", curve); + rv = APR_EGENERAL; goto leave; + } +#endif + rv = APR_SUCCESS; + break; + } + +leave: + if (APR_SUCCESS != rv) *ppkey = 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; @@ -473,6 +948,8 @@ apr_status_t md_pkey_gen(md_pkey_t **ppkey, apr_pool_t *p, md_pkey_spec_t *spec) return gen_rsa(ppkey, p, MD_PKEY_RSA_BITS_DEF); case MD_PKEY_TYPE_RSA: return gen_rsa(ppkey, p, spec->params.rsa.bits); + case MD_PKEY_TYPE_EC: + return gen_ec(ppkey, p, spec->params.ec.curve); default: return APR_ENOTIMPL; } @@ -501,59 +978,77 @@ static void RSA_get0_key(const RSA *r, 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); - } + md_data_t buffer; + + md_data_pinit(&buffer, (apr_size_t)BN_num_bytes(b), p); + if (buffer.data) { + BN_bn2bin(b, (unsigned char *)buffer.data); + return md_util_base64url_encode(&buffer, 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; +#if OPENSSL_VERSION_NUMBER < 0x30000000L + const RSA *rsa = EVP_PKEY_get0_RSA(pkey->pkey); + if (rsa) { + const BIGNUM *e; + RSA_get0_key(rsa, NULL, &e, NULL); + return bn64(e, p); + } +#else + BIGNUM *e = NULL; + if (EVP_PKEY_get_bn_param(pkey->pkey, OSSL_PKEY_PARAM_RSA_E, &e)) { + const char *e64 = bn64(e, p); + BN_free(e); + return e64; } - RSA_get0_key(rsa, NULL, &e, NULL); - return bn64(e, p); +#endif + return NULL; } 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; +#if OPENSSL_VERSION_NUMBER < 0x30000000L + const RSA *rsa = EVP_PKEY_get0_RSA(pkey->pkey); + if (rsa) { + const BIGNUM *n; + RSA_get0_key(rsa, &n, NULL, NULL); + return bn64(n, p); + } +#else + BIGNUM *n = NULL; + if (EVP_PKEY_get_bn_param(pkey->pkey, OSSL_PKEY_PARAM_RSA_N, &n)) { + const char *n64 = bn64(n, p); + BN_free(n); + return n64; } - RSA_get0_key(rsa, &n, NULL, NULL); - return bn64(n, p); +#endif + return NULL; } 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; + md_data_t 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) { + + md_data_pinit(&buffer, (apr_size_t)EVP_PKEY_size(pkey->pkey), p); + if (buffer.data) { 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 (EVP_SignFinal(ctx, (unsigned char*)buffer.data, &blen, pkey->pkey)) { + buffer.len = blen; + sign64 = md_util_base64url_encode(&buffer, p); if (sign64) { rv = APR_SUCCESS; } @@ -575,56 +1070,42 @@ apr_status_t md_crypt_sign64(const char **psign64, md_pkey_t *pkey, apr_pool_t * 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) +static apr_status_t sha256_digest(md_data_t **pdigest, apr_pool_t *p, const md_data_t *buf) { EVP_MD_CTX *ctx = NULL; - unsigned char *buffer; + md_data_t *digest; 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; - } + unsigned int dlen; + + digest = md_data_pmake(EVP_MAX_MD_SIZE, p); + 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, (unsigned char*)buf->data, buf->len)) { + if (EVP_DigestFinal(ctx, (unsigned char*)digest->data, &dlen)) { + digest->len = dlen; + 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; + if (ctx) { + EVP_MD_CTX_destroy(ctx); } + *pdigest = (APR_SUCCESS == rv)? digest : NULL; return rv; } -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_digest64(const char **pdigest64, apr_pool_t *p, const md_data_t *d) { const char *digest64 = NULL; - unsigned char *buffer; - size_t blen; + md_data_t *digest; 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))) { + if (APR_SUCCESS == (rv = sha256_digest(&digest, p, d))) { + if (NULL == (digest64 = md_util_base64url_encode(digest, p))) { rv = APR_EGENERAL; } } @@ -632,47 +1113,41 @@ apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, 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) + const md_data_t *data) { - char *dhex = NULL, *cp; - const char * x; - unsigned char *buffer; - size_t blen; + md_data_t *digest; 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]; - } + if (APR_SUCCESS == (rv = sha256_digest(&digest, p, data))) { + return md_data_to_hex(pdigesthex, 0, p, digest); } - *pdigesthex = dhex; + *pdigesthex = NULL; + return rv; +} + +apr_status_t md_crypt_hmac64(const char **pmac64, const md_data_t *hmac_key, + apr_pool_t *p, const char *d, size_t dlen) +{ + const char *mac64 = NULL; + unsigned char *s; + unsigned int digest_len = 0; + md_data_t *digest; + apr_status_t rv = APR_SUCCESS; + + digest = md_data_pmake(EVP_MAX_MD_SIZE, p); + s = HMAC(EVP_sha256(), (const unsigned char*)hmac_key->data, (int)hmac_key->len, + (const unsigned char*)d, (size_t)dlen, + (unsigned char*)digest->data, &digest_len); + if (!s) { + rv = APR_EINVAL; + goto cleanup; + } + digest->len = digest_len; + mac64 = md_util_base64url_encode(digest, p); + +cleanup: + *pmac64 = (APR_SUCCESS == rv)? mac64 : NULL; return rv; } @@ -695,26 +1170,47 @@ static apr_status_t cert_cleanup(void *data) return APR_SUCCESS; } -static md_cert_t *make_cert(apr_pool_t *p, X509 *x509) +md_cert_t *md_cert_wrap(apr_pool_t *p, void *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) +md_cert_t *md_cert_make(apr_pool_t *p, void *x509) { - cert_cleanup(cert); + md_cert_t *cert = md_cert_wrap(p, x509); + apr_pool_cleanup_register(p, cert, cert_cleanup, apr_pool_cleanup_null); + return cert; } -void *md_cert_get_X509(struct md_cert_t *cert) +void *md_cert_get_X509(const md_cert_t *cert) { return cert->x509; } +const char *md_cert_get_serial_number(const md_cert_t *cert, apr_pool_t *p) +{ + const char *s = ""; + BIGNUM *bn; + const char *serial; + const ASN1_INTEGER *ai = X509_get_serialNumber(cert->x509); + if (ai) { + bn = ASN1_INTEGER_to_BN(ai, NULL); + serial = BN_bn2hex(bn); + s = apr_pstrdup(p, serial); + OPENSSL_free((void*)serial); + OPENSSL_free((void*)bn); + } + return s; +} + +int md_certs_are_equal(const md_cert_t *a, const md_cert_t *b) +{ + return X509_cmp(a->x509, b->x509) == 0; +} + int md_cert_is_valid_now(const md_cert_t *cert) { return ((X509_cmp_current_time(X509_get_notBefore(cert->x509)) < 0) @@ -726,23 +1222,31 @@ 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) +apr_time_t md_cert_get_not_after(const 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) +apr_time_t md_cert_get_not_before(const md_cert_t *cert) { return md_asn1_time_get(X509_get_notBefore(cert->x509)); } +md_timeperiod_t md_cert_get_valid(const md_cert_t *cert) +{ + md_timeperiod_t p; + p.start = md_cert_get_not_before(cert); + p.end = md_cert_get_not_after(cert); + return p; +} + 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; + apr_array_header_t *alt_names; + + md_cert_get_alt_names(&alt_names, cert, cert->pool); + if (alt_names) { + return md_array_str_index(alt_names, domain_name, 0, 0) >= 0; } return 0; } @@ -760,7 +1264,7 @@ int md_cert_covers_md(md_cert_t *cert, const md_t *md) 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) { + if (!md_dns_domains_match(cert->alt_names, name)) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, cert->pool, "md domain %s not covered by cert", name); return 0; @@ -774,7 +1278,7 @@ int md_cert_covers_md(md_cert_t *cert, const md_t *md) return 0; } -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_issuers_uri(const char **puri, const md_cert_t *cert, apr_pool_t *p) { apr_status_t rv = APR_ENOENT; STACK_OF(ACCESS_DESCRIPTION) *xinfos; @@ -801,17 +1305,19 @@ apr_status_t md_cert_get_issuers_uri(const char **puri, md_cert_t *cert, apr_poo return rv; } -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_get_alt_names(apr_array_header_t **pnames, const 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; + const unsigned char *ip; + int len; names = apr_array_make(p, sk_GENERAL_NAME_num(xalt_names), sizeof(char *)); for (i = 0; i < sk_GENERAL_NAME_num(xalt_names); ++i) { @@ -819,11 +1325,33 @@ apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, md_cert_t *cert, 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; + case GEN_IPADD: + len = ASN1_STRING_length(cval->d.iPAddress); +#if OPENSSL_VERSION_NUMBER < 0x10100000L + ip = ASN1_STRING_data(cval->d.iPAddress); +#else + ip = ASN1_STRING_get0_data(cval->d.iPAddress); +#endif + if (len == 4) /* IPv4 address */ + APR_ARRAY_PUSH(names, const char *) = apr_psprintf(p, "%u.%u.%u.%u", + ip[0], ip[1], ip[2], ip[3]); + else if (len == 16) /* IPv6 address */ + APR_ARRAY_PUSH(names, const char *) = apr_psprintf(p, "%02x%02x%02x%02x:" + "%02x%02x%02x%02x:" + "%02x%02x%02x%02x:" + "%02x%02x%02x%02x", + ip[0], ip[1], ip[2], ip[3], + ip[4], ip[5], ip[6], ip[7], + ip[8], ip[9], ip[10], ip[11], + ip[12], ip[13], ip[14], ip[15]); + else { + ; /* Unknown address type - Log? Assert? */ + } + break; default: break; } @@ -848,7 +1376,7 @@ apr_status_t md_cert_fload(md_cert_t **pcert, apr_pool_t *p, const char *fname) x509 = PEM_read_X509(f, NULL, NULL, NULL); rv = fclose(f); if (x509 != NULL) { - cert = make_cert(p, x509); + cert = md_cert_make(p, x509); } else { rv = APR_EINVAL; @@ -859,7 +1387,7 @@ apr_status_t md_cert_fload(md_cert_t **pcert, apr_pool_t *p, const char *fname) return rv; } -static apr_status_t cert_to_buffer(buffer_rec *buffer, md_cert_t *cert, apr_pool_t *p) +static apr_status_t cert_to_buffer(md_data_t *buffer, const md_cert_t *cert, apr_pool_t *p) { BIO *bio = BIO_new(BIO_s_mem()); int i; @@ -877,9 +1405,8 @@ static apr_status_t cert_to_buffer(buffer_rec *buffer, md_cert_t *cert, apr_pool 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->data = apr_palloc(p, (apr_size_t)i); + i = BIO_read(bio, (char*)buffer->data, i); buffer->len = (apr_size_t)i; } BIO_free(bio); @@ -889,64 +1416,194 @@ static apr_status_t cert_to_buffer(buffer_rec *buffer, md_cert_t *cert, apr_pool apr_status_t md_cert_fsave(md_cert_t *cert, apr_pool_t *p, const char *fname, apr_fileperms_t perms) { - buffer_rec buffer; + md_data_t buffer; apr_status_t rv; - + + md_data_null(&buffer); 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) +apr_status_t md_cert_to_base64url(const char **ps64, const md_cert_t *cert, apr_pool_t *p) { - buffer_rec buffer; + md_data_t buffer; apr_status_t rv; - + + md_data_null(&buffer); if (APR_SUCCESS == (rv = cert_to_buffer(&buffer, cert, p))) { - *ps64 = md_util_base64url_encode(buffer.data, buffer.len, p); + *ps64 = md_util_base64url_encode(&buffer, p); return APR_SUCCESS; } *ps64 = NULL; return rv; } +apr_status_t md_cert_to_sha256_digest(md_data_t **pdigest, const md_cert_t *cert, apr_pool_t *p) +{ + md_data_t *digest; + unsigned int dlen; + + digest = md_data_pmake(EVP_MAX_MD_SIZE, p); + X509_digest(cert->x509, EVP_sha256(), (unsigned char*)digest->data, &dlen); + digest->len = dlen; + + *pdigest = digest; + return APR_SUCCESS; +} + +apr_status_t md_cert_to_sha256_fingerprint(const char **pfinger, const md_cert_t *cert, apr_pool_t *p) +{ + md_data_t *digest; + apr_status_t rv; + + rv = md_cert_to_sha256_digest(&digest, cert, p); + if (APR_SUCCESS == rv) { + return md_data_to_hex(pfinger, 0, p, digest); + } + *pfinger = NULL; + return rv; +} + +static int md_cert_read_pem(BIO *bf, apr_pool_t *p, md_cert_t **pcert) +{ + md_cert_t *cert; + X509 *x509; + apr_status_t rv = APR_ENOENT; + + ERR_clear_error(); + x509 = PEM_read_bio_X509(bf, NULL, NULL, NULL); + if (x509 == NULL) goto cleanup; + cert = md_cert_make(p, x509); + rv = APR_SUCCESS; +cleanup: + *pcert = (APR_SUCCESS == rv)? cert : NULL; + return rv; +} + +apr_status_t md_cert_read_chain(apr_array_header_t *chain, apr_pool_t *p, + const char *pem, apr_size_t pem_len) +{ + BIO *bf = NULL; + apr_status_t rv = APR_SUCCESS; + md_cert_t *cert; + int added = 0; + + if (NULL == (bf = BIO_new_mem_buf(pem, (int)pem_len))) { + rv = APR_ENOMEM; + goto cleanup; + } + while (APR_SUCCESS == (rv = md_cert_read_pem(bf, chain->pool, &cert))) { + APR_ARRAY_PUSH(chain, md_cert_t *) = cert; + added = 1; + } + if (APR_ENOENT == rv && added) { + rv = APR_SUCCESS; + } + +cleanup: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p, "read chain with %d certs", chain->nelts); + if (bf) BIO_free(bf); + 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; + char *der; apr_size_t der_len; + md_cert_t *cert = NULL; apr_status_t rv; ct = apr_table_get(res->headers, "Content-Type"); - if (!res->body || !ct || strcmp("application/pkix-cert", ct)) { - return APR_ENOENT; + ct = md_util_parse_ct(res->req->pool, ct); + if (!res->body || !ct || strcmp("application/pkix-cert", ct)) { + rv = APR_ENOENT; + goto out; } 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))) { + if (APR_SUCCESS == (rv = apr_brigade_pflatten(res->body, &der, &der_len, res->req->pool))) { const unsigned char *bf = (const unsigned char*)der; X509 *x509; if (NULL == (x509 = d2i_X509(NULL, &bf, (long)der_len))) { rv = APR_EINVAL; + goto out; } else { - *pcert = make_cert(p, x509); + cert = md_cert_make(p, x509); rv = APR_SUCCESS; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p, + "parsing cert from content-type=%s, content-length=%ld", ct, (long)data_len); } } - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "cert parsed"); } +out: + *pcert = (APR_SUCCESS == rv)? cert : NULL; return rv; } -md_cert_state_t md_cert_state_get(md_cert_t *cert) +apr_status_t md_cert_chain_read_http(struct apr_array_header_t *chain, + apr_pool_t *p, const struct md_http_response_t *res) +{ + const char *ct = NULL; + apr_off_t blen; + apr_size_t data_len = 0; + char *data; + md_cert_t *cert; + apr_status_t rv = APR_ENOENT; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, p, + "chain_read, processing %d response", res->status); + if (APR_SUCCESS != (rv = apr_brigade_length(res->body, 1, &blen))) goto cleanup; + if (blen > 1024*1024) { /* certs usually are <2k each */ + rv = APR_EINVAL; + goto cleanup; + } + + data_len = (apr_size_t)blen; + ct = apr_table_get(res->headers, "Content-Type"); + if (!res->body || !ct) goto cleanup; + ct = md_util_parse_ct(res->req->pool, ct); + if (!strcmp("application/pkix-cert", ct)) { + rv = md_cert_read_http(&cert, p, res); + if (APR_SUCCESS != rv) goto cleanup; + APR_ARRAY_PUSH(chain, md_cert_t *) = cert; + } + else if (!strcmp("application/pem-certificate-chain", ct) + || !strncmp("text/plain", ct, sizeof("text/plain")-1)) { + /* Some servers seem to think 'text/plain' is sufficient, see #232 */ + rv = apr_brigade_pflatten(res->body, &data, &data_len, res->req->pool); + if (APR_SUCCESS != rv) goto cleanup; + rv = md_cert_read_chain(chain, res->req->pool, data, data_len); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "attempting to parse certificates from unrecognized content-type: %s", ct); + rv = apr_brigade_pflatten(res->body, &data, &data_len, res->req->pool); + if (APR_SUCCESS != rv) goto cleanup; + rv = md_cert_read_chain(chain, res->req->pool, data, data_len); + if (APR_SUCCESS == rv && chain->nelts == 0) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "certificate chain response did not contain any certificates " + "(suspicious content-type: %s)", ct); + rv = APR_ENOENT; + } + } +cleanup: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p, + "parsed certs from content-type=%s, content-length=%ld", ct, (long)data_len); + return rv; +} + +md_cert_state_t md_cert_state_get(const md_cert_t *cert) { if (cert->x509) { return md_cert_is_valid_now(cert)? MD_CERT_VALID : MD_CERT_EXPIRED; @@ -966,7 +1623,7 @@ apr_status_t md_chain_fappend(struct apr_array_header_t *certs, apr_pool_t *p, c if (rv == APR_SUCCESS) { ERR_clear_error(); while (NULL != (x509 = PEM_read_X509(f, NULL, NULL, NULL))) { - cert = make_cert(p, x509); + cert = md_cert_make(p, x509); APR_ARRAY_PUSH(certs, md_cert_t *) = cert; } fclose(f); @@ -1064,18 +1721,22 @@ static apr_status_t add_ext(X509 *x, int nid, const char *value, apr_pool_t *p) X509V3_CTX ctx; apr_status_t rv; + ERR_clear_error(); 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))) { + unsigned long err = ERR_get_error(); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "add_ext, create, nid=%d value='%s' " + "(lib=%d, reason=%d)", nid, value, ERR_GET_LIB(err), ERR_GET_REASON(err)); 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); - + unsigned long err = ERR_get_error(); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "add_ext, add, nid=%d value='%s' " + "(lib=%d, reason=%d)", nid, value, ERR_GET_LIB(err), ERR_GET_REASON(err)); } X509_EXTENSION_free(ext); return rv; @@ -1100,116 +1761,114 @@ static apr_status_t sk_add_alt_names(STACK_OF(X509_EXTENSION) *exts, #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) +int md_cert_must_staple(const 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(); + int nid = MD_GET_NID(MUST_STAPLE); 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) +static apr_status_t add_must_staple(STACK_OF(X509_EXTENSION) *exts, const char *name, apr_pool_t *p) { + X509_EXTENSION *x; + int nid; - 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); + nid = MD_GET_NID(MUST_STAPLE); + 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", 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", 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, +apr_status_t md_cert_req_create(const char **pcsr_der_64, const char *name, + apr_array_header_t *domains, int must_staple, md_pkey_t *pkey, apr_pool_t *p) { - const char *s, *csr_der, *csr_der_64 = NULL; + const char *s, *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; + md_data_t csr_der; int csr_der_len; - assert(md->domains->nelts > 0); - + assert(domains->nelts > 0); + md_data_null(&csr_der); + 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); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: openssl alloc X509 things", 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); + domain = APR_ARRAY_IDX(domains, 0, const unsigned char *); + /* Do not set the domain in the CN if it is longer than 64 octets. + * Instead, let the CA choose a 'proper' name. At the moment (2021-01), LE will + * inspect all SAN names and use one < 64 chars if it can be found. It will fail + * otherwise. + * The reason we do not check this beforehand is that the restrictions on CNs + * are in flux. They used to be authoritative, now browsers no longer do that, but + * no one wants to hand out a cert with "google.com" as CN either. So, we leave + * it for the CA to decide if and how it hands out a cert for this or fails. + * This solves issue where the name is too long, see #227 */ + if (strlen((const char*)domain) < 64 + && (!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", 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); + if (APR_SUCCESS != (rv = sk_add_alt_names(exts, domains, p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: collecting alt names", name); rv = APR_EGENERAL; goto out; } - if (APR_SUCCESS != (rv = add_must_staple(exts, md, p))) { + if (must_staple && APR_SUCCESS != (rv = add_must_staple(exts, name, 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); + "configure 'MDMustStaple off' for your domains", 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); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: adding exts", 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); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set pkey in csr", 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); + if (!X509_REQ_sign(csr, pkey->pkey, pkey_get_MD(pkey))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign csr", 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); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: der length", name); rv = APR_EGENERAL; goto out; } - s = csr_der = apr_pcalloc(p, (apr_size_t)csr_der_len + 1); + csr_der.len = (apr_size_t)csr_der_len; + s = csr_der.data = apr_pcalloc(p, 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); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: csr der enc", name); rv = APR_EGENERAL; goto out; } - csr_der_64 = md_util_base64url_encode(csr_der, (apr_size_t)csr_der_len, p); + csr_der_64 = md_util_base64url_encode(&csr_der, p); rv = APR_SUCCESS; out: @@ -1226,20 +1885,16 @@ out: 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) +static apr_status_t mk_x509(X509 **px, md_pkey_t *pkey, const char *cn, + apr_interval_time_t valid_for, apr_pool_t *p) { - X509 *x; + X509 *x = NULL; 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); + int days; + apr_status_t rv; if (NULL == (x = X509_new()) || NULL == (n = X509_NAME_new())) { @@ -1247,24 +1902,22 @@ apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, 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 (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; + } + /* set common name and issuer */ 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)) { @@ -1272,21 +1925,16 @@ apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *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))) { + if (APR_SUCCESS != (rv = add_ext(x, NID_basic_constraints, "critical,CA:FALSE", 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; } - + /* validity */ 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; @@ -1295,29 +1943,217 @@ apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, rv = APR_EGENERAL; goto out; } +out: + *px = (APR_SUCCESS == rv)? x : NULL; + if (APR_SUCCESS != rv && x) X509_free(x); + if (big_rnd) BN_free(big_rnd); + if (asn1_rnd) ASN1_INTEGER_free(asn1_rnd); + if (n) X509_NAME_free(n); + 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; + md_cert_t *cert = NULL; + apr_status_t rv; + + assert(domains); + + if (APR_SUCCESS != (rv = mk_x509(&x, pkey, cn, valid_for, p))) 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; + } + + /* keyUsage, ExtendedKeyUsage */ + + if (APR_SUCCESS != (rv = add_ext(x, NID_key_usage, "critical,digitalSignature", p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set keyUsage", cn); + goto out; + } + if (APR_SUCCESS != (rv = add_ext(x, NID_ext_key_usage, "serverAuth", p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set extKeyUsage", cn); + goto out; + } + /* sign with same key */ - if (!X509_sign(x, pkey->pkey, EVP_sha256())) { + if (!X509_sign(x, pkey->pkey, pkey_get_MD(pkey))) { 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); + cert = md_cert_make(p, x); + rv = APR_SUCCESS; + +out: + *pcert = (APR_SUCCESS == rv)? cert : NULL; + if (!cert && x) X509_free(x); + return rv; +} + +#define MD_OID_ACME_VALIDATION_NUM "1.3.6.1.5.5.7.1.31" +#define MD_OID_ACME_VALIDATION_SNAME "pe-acmeIdentifier" +#define MD_OID_ACME_VALIDATION_LNAME "ACME Identifier" + +static int get_acme_validation_nid(void) +{ + int nid = OBJ_txt2nid(MD_OID_ACME_VALIDATION_NUM); + if (NID_undef == nid) { + nid = OBJ_create(MD_OID_ACME_VALIDATION_NUM, + MD_OID_ACME_VALIDATION_SNAME, MD_OID_ACME_VALIDATION_LNAME); + } + return nid; +} + +apr_status_t md_cert_make_tls_alpn_01(md_cert_t **pcert, const char *domain, + const char *acme_id, md_pkey_t *pkey, + apr_interval_time_t valid_for, apr_pool_t *p) +{ + X509 *x; + md_cert_t *cert = NULL; + const char *alts; + apr_status_t rv; + + if (APR_SUCCESS != (rv = mk_x509(&x, pkey, "tls-alpn-01-challenge", valid_for, p))) goto out; + + /* add the domain as alt name */ + alts = apr_psprintf(p, "DNS:%s", domain); + if (APR_SUCCESS != (rv = add_ext(x, NID_subject_alt_name, alts, p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set alt_name ext", domain); + goto out; + } + + if (APR_SUCCESS != (rv = add_ext(x, get_acme_validation_nid(), acme_id, p))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set pe-acmeIdentifier", domain); + goto out; + } + + /* sign with same key */ + if (!X509_sign(x, pkey->pkey, pkey_get_MD(pkey))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign x509", domain); + rv = APR_EGENERAL; goto out; + } + + cert = md_cert_make(p, x); rv = APR_SUCCESS; out: if (!cert && x) { X509_free(x); } - if (n) { - X509_NAME_free(n); + *pcert = (APR_SUCCESS == rv)? cert : NULL; + return rv; +} + +#define MD_OID_CT_SCTS_NUM "1.3.6.1.4.1.11129.2.4.2" +#define MD_OID_CT_SCTS_SNAME "CT-SCTs" +#define MD_OID_CT_SCTS_LNAME "CT Certificate SCTs" + +#ifndef OPENSSL_NO_CT +static int get_ct_scts_nid(void) +{ + int nid = OBJ_txt2nid(MD_OID_CT_SCTS_NUM); + if (NID_undef == nid) { + nid = OBJ_create(MD_OID_CT_SCTS_NUM, + MD_OID_CT_SCTS_SNAME, MD_OID_CT_SCTS_LNAME); } - if (big_rnd) { - BN_free(big_rnd); + return nid; +} +#endif + +const char *md_nid_get_sname(int nid) +{ + return OBJ_nid2sn(nid); +} + +const char *md_nid_get_lname(int nid) +{ + return OBJ_nid2ln(nid); +} + +apr_status_t md_cert_get_ct_scts(apr_array_header_t *scts, apr_pool_t *p, const md_cert_t *cert) +{ +#ifndef OPENSSL_NO_CT + int nid, i, idx, critical; + STACK_OF(SCT) *sct_list; + SCT *sct_handle; + md_sct *sct; + size_t len; + const char *data; + + nid = get_ct_scts_nid(); + if (NID_undef == nid) return APR_ENOTIMPL; + + idx = -1; + while (1) { + sct_list = X509_get_ext_d2i(cert->x509, nid, &critical, &idx); + if (sct_list) { + for (i = 0; i < sk_SCT_num(sct_list); i++) { + sct_handle = sk_SCT_value(sct_list, i); + if (sct_handle) { + sct = apr_pcalloc(p, sizeof(*sct)); + sct->version = SCT_get_version(sct_handle); + sct->timestamp = apr_time_from_msec(SCT_get_timestamp(sct_handle)); + len = SCT_get0_log_id(sct_handle, (unsigned char**)&data); + sct->logid = md_data_make_pcopy(p, data, len); + sct->signature_type_nid = SCT_get_signature_nid(sct_handle); + len = SCT_get0_signature(sct_handle, (unsigned char**)&data); + sct->signature = md_data_make_pcopy(p, data, len); + + APR_ARRAY_PUSH(scts, md_sct*) = sct; + } + } + } + if (idx < 0) break; } - if (asn1_rnd) { - ASN1_INTEGER_free(asn1_rnd); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "ct_sct, found %d SCT extensions", scts->nelts); + return APR_SUCCESS; +#else + (void)scts; + (void)p; + (void)cert; + return APR_ENOTIMPL; +#endif +} + +apr_status_t md_cert_get_ocsp_responder_url(const char **purl, apr_pool_t *p, const md_cert_t *cert) +{ + STACK_OF(OPENSSL_STRING) *ssk; + apr_status_t rv = APR_SUCCESS; + const char *url = NULL; + + ssk = X509_get1_ocsp(md_cert_get_X509(cert)); + if (!ssk) { + rv = APR_ENOENT; + goto cleanup; } - *pcert = (APR_SUCCESS == rv)? cert : NULL; + url = apr_pstrdup(p, sk_OPENSSL_STRING_value(ssk, 0)); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, p, "ocsp responder found '%s'", url); + +cleanup: + if (ssk) X509_email_free(ssk); + *purl = url; return rv; } +apr_status_t md_check_cert_and_pkey(struct apr_array_header_t *certs, md_pkey_t *pkey) +{ + const md_cert_t *cert; + + if (certs->nelts == 0) { + return APR_ENOENT; + } + + cert = APR_ARRAY_IDX(certs, 0, const md_cert_t*); + + if (1 != X509_check_private_key(cert->x509, pkey->pkey)) { + return APR_EGENERAL; + } + + return APR_SUCCESS; +} diff --git a/modules/md/md_crypt.h b/modules/md/md_crypt.h index e03c296..a892e00 100644 --- a/modules/md/md_crypt.h +++ b/modules/md/md_crypt.h @@ -24,18 +24,22 @@ struct md_t; struct md_http_response_t; struct md_cert_t; struct md_pkey_t; +struct md_data_t; +struct md_timeperiod_t; /**************************************************************************************************/ /* random */ apr_status_t md_rand_bytes(unsigned char *buf, apr_size_t len, apr_pool_t *p); +apr_time_t md_asn1_generalized_time_get(void *ASN1_GENERALIZEDTIME); + /**************************************************************************************************/ /* digests */ apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, - const char *d, size_t dlen); + const struct md_data_t *data); apr_status_t md_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p, - const char *d, size_t dlen); + const struct md_data_t *data); /**************************************************************************************************/ /* private keys */ @@ -45,22 +49,54 @@ typedef struct md_pkey_t md_pkey_t; typedef enum { MD_PKEY_TYPE_DEFAULT, MD_PKEY_TYPE_RSA, + MD_PKEY_TYPE_EC, } md_pkey_type_t; -typedef struct md_pkey_rsa_spec_t { +typedef struct md_pkey_rsa_params_t { apr_uint32_t bits; -} md_pkey_rsa_spec_t; +} md_pkey_rsa_params_t; + +typedef struct md_pkey_ec_params_t { + const char *curve; +} md_pkey_ec_params_t; typedef struct md_pkey_spec_t { md_pkey_type_t type; union { - md_pkey_rsa_spec_t rsa; + md_pkey_rsa_params_t rsa; + md_pkey_ec_params_t ec; } params; } md_pkey_spec_t; +typedef struct md_pkeys_spec_t { + apr_pool_t *p; + struct apr_array_header_t *specs; +} md_pkeys_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); +const char *md_pkey_spec_name(const md_pkey_spec_t *spec); + +md_pkeys_spec_t *md_pkeys_spec_make(apr_pool_t *p); +void md_pkeys_spec_add_default(md_pkeys_spec_t *pks); +int md_pkeys_spec_contains_rsa(md_pkeys_spec_t *pks); +void md_pkeys_spec_add_rsa(md_pkeys_spec_t *pks, unsigned int bits); +int md_pkeys_spec_contains_ec(md_pkeys_spec_t *pks, const char *curve); +void md_pkeys_spec_add_ec(md_pkeys_spec_t *pks, const char *curve); +int md_pkeys_spec_eq(md_pkeys_spec_t *pks1, md_pkeys_spec_t *pks2); +md_pkeys_spec_t *md_pkeys_spec_clone(apr_pool_t *p, const md_pkeys_spec_t *pks); +int md_pkeys_spec_is_empty(const md_pkeys_spec_t *pks); +md_pkey_spec_t *md_pkeys_spec_get(const md_pkeys_spec_t *pks, int index); +int md_pkeys_spec_count(const md_pkeys_spec_t *pks); +void md_pkeys_spec_add(md_pkeys_spec_t *pks, md_pkey_spec_t *spec); + +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); +struct md_json_t *md_pkeys_spec_to_json(const md_pkeys_spec_t *pks, apr_pool_t *p); +md_pkeys_spec_t *md_pkeys_spec_from_json(struct md_json_t *json, apr_pool_t *p); + + +apr_status_t md_pkey_gen(md_pkey_t **ppkey, apr_pool_t *p, md_pkey_spec_t *key_props); void md_pkey_free(md_pkey_t *pkey); const char *md_pkey_get_rsa_e64(md_pkey_t *pkey, apr_pool_t *p); @@ -76,12 +112,16 @@ apr_status_t md_pkey_fsave(md_pkey_t *pkey, apr_pool_t *p, 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); +apr_status_t md_crypt_hmac64(const char **pmac64, const struct md_data_t *hmac_key, + apr_pool_t *p, const char *d, size_t dlen); + +/** + * Read a private key from a http response. + */ +apr_status_t md_pkey_read_http(md_pkey_t **ppkey, apr_pool_t *pool, + const struct md_http_response_t *res); /**************************************************************************************************/ /* X509 certificates */ @@ -94,30 +134,73 @@ typedef enum { MD_CERT_EXPIRED } md_cert_state_t; -void md_cert_free(md_cert_t *cert); +/** + * Create a holder of the certificate that will free its memory when the + * pool is destroyed. + */ +md_cert_t *md_cert_make(apr_pool_t *p, void *x509); + +/** + * Wrap a x509 certificate into our own structure, without taking ownership + * of its memory. The caller remains responsible. + */ +md_cert_t *md_cert_wrap(apr_pool_t *p, void *x509); + +void *md_cert_get_X509(const 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); +/** + * Read a x509 certificate from a http response. + * Will return APR_ENOENT if content-type is not recognized (currently + * only "application/pkix-cert" is supported). + */ 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); +/** + * Read at least one certificate from the given PEM data. + */ +apr_status_t md_cert_read_chain(apr_array_header_t *chain, apr_pool_t *p, + const char *pem, apr_size_t pem_len); + +/** + * Read one or even a chain of certificates from a http response. + * Will return APR_ENOENT if content-type is not recognized (currently + * supports only "application/pem-certificate-chain" and "application/pkix-cert"). + * @param chain must be non-NULL, retrieved certificates will be added. + */ +apr_status_t md_cert_chain_read_http(struct apr_array_header_t *chain, + apr_pool_t *pool, const struct md_http_response_t *res); + +md_cert_state_t md_cert_state_get(const 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); +int md_cert_must_staple(const md_cert_t *cert); +apr_time_t md_cert_get_not_after(const md_cert_t *cert); +apr_time_t md_cert_get_not_before(const md_cert_t *cert); +struct md_timeperiod_t md_cert_get_valid(const 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); +/** + * Return != 0 iff the hash values of the certificates are equal. + */ +int md_certs_are_equal(const md_cert_t *a, const md_cert_t *b); + +apr_status_t md_cert_get_issuers_uri(const char **puri, const md_cert_t *cert, apr_pool_t *p); +apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, const 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_to_base64url(const char **ps64, const 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_cert_to_sha256_digest(struct md_data_t **pdigest, const md_cert_t *cert, apr_pool_t *p); +apr_status_t md_cert_to_sha256_fingerprint(const char **pfinger, const md_cert_t *cert, apr_pool_t *p); + +const char *md_cert_get_serial_number(const md_cert_t *cert, 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, @@ -125,11 +208,46 @@ apr_status_t md_chain_fsave(struct apr_array_header_t *certs, 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, +apr_status_t md_cert_req_create(const char **pcsr_der_64, const char *name, + apr_array_header_t *domains, int must_staple, md_pkey_t *pkey, apr_pool_t *p); +/** + * Create a self-signed cerftificate with the given cn, key and list + * of alternate domain names. + */ 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); + +/** + * Create a certificate for answering "tls-alpn-01" ACME challenges + * (see <https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01>). + */ +apr_status_t md_cert_make_tls_alpn_01(md_cert_t **pcert, const char *domain, + const char *acme_id, md_pkey_t *pkey, + apr_interval_time_t valid_for, apr_pool_t *p); + +apr_status_t md_cert_get_ct_scts(apr_array_header_t *scts, apr_pool_t *p, const md_cert_t *cert); + +apr_status_t md_cert_get_ocsp_responder_url(const char **purl, apr_pool_t *p, const md_cert_t *cert); + +apr_status_t md_check_cert_and_pkey(struct apr_array_header_t *certs, md_pkey_t *pkey); + + +/**************************************************************************************************/ +/* X509 certificate transparency */ + +const char *md_nid_get_sname(int nid); +const char *md_nid_get_lname(int nid); + +typedef struct md_sct md_sct; +struct md_sct { + int version; + apr_time_t timestamp; + struct md_data_t *logid; + int signature_type_nid; + struct md_data_t *signature; +}; #endif /* md_crypt_h */ diff --git a/modules/md/md_curl.c b/modules/md/md_curl.c index f3585da..217e857 100644 --- a/modules/md/md_curl.c +++ b/modules/md/md_curl.c @@ -24,13 +24,14 @@ #include "md_http.h" #include "md_log.h" +#include "md_util.h" #include "md_curl.h" /**************************************************************************************************/ /* md_http curl implementation */ -static apr_status_t curl_status(int curl_code) +static apr_status_t curl_status(unsigned int curl_code) { switch (curl_code) { case CURLE_OK: return APR_SUCCESS; @@ -49,11 +50,21 @@ static apr_status_t curl_status(int curl_code) } } +typedef struct { + CURL *curl; + CURLM *curlm; + struct curl_slist *req_hdrs; + md_http_response_t *response; + apr_status_t rv; + int status_fired; +} md_curl_internals_t; + 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; + char *rdata = data; apr_bucket *b; apr_status_t rv; @@ -71,9 +82,10 @@ static size_t req_data_cb(void *data, size_t len, size_t nmemb, void *baton) apr_bucket_split(b, max_len); blen = max_len; } - memcpy(data, bdata, blen); + memcpy(rdata, bdata, blen); read_len += blen; max_len -= blen; + rdata += blen; } else { body = NULL; @@ -92,7 +104,8 @@ static size_t req_data_cb(void *data, size_t len, size_t nmemb, void *baton) static size_t resp_data_cb(void *data, size_t len, size_t nmemb, void *baton) { - md_http_response_t *res = baton; + md_curl_internals_t *internals = baton; + md_http_response_t *res = internals->response; size_t blen = len * nmemb; apr_status_t rv; @@ -100,7 +113,7 @@ static size_t resp_data_cb(void *data, size_t len, size_t nmemb, void *baton) 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) { + if (body_len + (apr_off_t)blen > res->req->resp_limit) { return 0; /* signal curl failure */ } } @@ -115,7 +128,8 @@ static size_t resp_data_cb(void *data, size_t len, size_t nmemb, void *baton) static size_t header_cb(void *buffer, size_t elen, size_t nmemb, void *baton) { - md_http_response_t *res = baton; + md_curl_internals_t *internals = baton; + md_http_response_t *res = internals->response; size_t len, clen = elen * nmemb; const char *name = NULL, *value = "", *b = buffer; apr_size_t i; @@ -142,24 +156,6 @@ static size_t header_cb(void *buffer, size_t elen, size_t nmemb, void *baton) 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; @@ -181,24 +177,101 @@ static int curlify_headers(void *baton, const char *key, const char *value) return 1; } -static apr_status_t curl_perform(md_http_request_t *req) +/* Convert timeout values for curl. Since curl uses 0 to disable + * timeout, return at least 1 if the apr_time_t value is non-zero. */ +static long timeout_msec(apr_time_t timeout) { - apr_status_t rv = APR_SUCCESS; - CURLcode curle; - md_http_response_t *res; - CURL *curl; - struct curl_slist *req_hdrs = NULL; + long ms = (long)apr_time_as_msec(timeout); + return ms? ms : (timeout? 1 : 0); +} - rv = curl_init(req); - curl = req->internals; - - res = apr_pcalloc(req->pool, sizeof(*res)); +static long timeout_sec(apr_time_t timeout) +{ + long s = (long)apr_time_sec(timeout); + return s? s : (timeout? 1 : 0); +} + +static int curl_debug_log(CURL *curl, curl_infotype type, char *data, size_t size, void *baton) +{ + md_http_request_t *req = baton; - 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); + (void)curl; + switch (type) { + case CURLINFO_TEXT: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, + "req[%d]: info %s", req->id, apr_pstrndup(req->pool, data, size)); + break; + case CURLINFO_HEADER_OUT: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, + "req[%d]: header --> %s", req->id, apr_pstrndup(req->pool, data, size)); + break; + case CURLINFO_HEADER_IN: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, + "req[%d]: header <-- %s", req->id, apr_pstrndup(req->pool, data, size)); + break; + case CURLINFO_DATA_OUT: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, + "req[%d]: data --> %ld bytes", req->id, (long)size); + if (md_log_is_level(req->pool, MD_LOG_TRACE5)) { + md_data_t d; + const char *s; + md_data_init(&d, data, size); + md_data_to_hex(&s, 0, req->pool, &d); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE5, 0, req->pool, + "req[%d]: data(hex) --> %s", req->id, s); + } + break; + case CURLINFO_DATA_IN: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, + "req[%d]: data <-- %ld bytes", req->id, (long)size); + if (md_log_is_level(req->pool, MD_LOG_TRACE5)) { + md_data_t d; + const char *s; + md_data_init(&d, data, size); + md_data_to_hex(&s, 0, req->pool, &d); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE5, 0, req->pool, + "req[%d]: data(hex) <-- %s", req->id, s); + } + break; + default: + break; + } + return 0; +} + +static apr_status_t internals_setup(md_http_request_t *req) +{ + md_curl_internals_t *internals; + CURL *curl; + apr_status_t rv = APR_SUCCESS; + + curl = md_http_get_impl_data(req->http); + if (!curl) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, "creating curl instance"); + curl = curl_easy_init(); + if (!curl) { + rv = APR_EGENERAL; + goto leave; + } + 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); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, "reusing curl instance from http"); + } + + internals = apr_pcalloc(req->pool, sizeof(*internals)); + internals->curl = curl; + + internals->response = apr_pcalloc(req->pool, sizeof(md_http_response_t)); + internals->response->req = req; + internals->response->status = 400; + internals->response->headers = apr_table_make(req->pool, 5); + internals->response->body = apr_brigade_create(req->pool, req->bucket_alloc); curl_easy_setopt(curl, CURLOPT_URL, req->url); if (!apr_strnatcasecmp("GET", req->method)) { @@ -213,9 +286,32 @@ static apr_status_t curl_perform(md_http_request_t *req) else { curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->method); } - curl_easy_setopt(curl, CURLOPT_HEADERDATA, res); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, internals); curl_easy_setopt(curl, CURLOPT_READDATA, req->body); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, res); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, internals); + + if (req->timeout.overall > 0) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_msec(req->timeout.overall)); + } + if (req->timeout.connect > 0) { + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_msec(req->timeout.connect)); + } + if (req->timeout.stalled > 0) { + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, req->timeout.stall_bytes_per_sec); + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, timeout_sec(req->timeout.stalled)); + } + if (req->ca_file) { + curl_easy_setopt(curl, CURLOPT_CAINFO, req->ca_file); + } + if (req->unix_socket_path) { + curl_easy_setopt(curl, CURLOPT_UNIX_SOCKET_PATH, req->unix_socket_path); + } + + if (req->body_len >= 0) { + /* set the Content-Length */ + curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)req->body_len); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)req->body_len); + } if (req->user_agent) { curl_easy_setopt(curl, CURLOPT_USERAGENT, req->user_agent); @@ -230,47 +326,267 @@ static apr_status_t curl_perform(md_http_request_t *req) ctx.hdrs = NULL; ctx.rv = APR_SUCCESS; apr_table_do(curlify_headers, &ctx, req->headers, NULL); - req_hdrs = ctx.hdrs; + internals->req_hdrs = ctx.hdrs; if (ctx.rv == APR_SUCCESS) { - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_hdrs); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, internals->req_hdrs); } } - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->pool, - "request %ld --> %s %s", req->id, req->method, req->url); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, + "req[%d]: %s %s", req->id, req->method, req->url); - if (md_log_is_level(req->pool, MD_LOG_TRACE3)) { + if (md_log_is_level(req->pool, MD_LOG_TRACE4)) { curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, curl_debug_log); + curl_easy_setopt(curl, CURLOPT_DEBUGDATA, req); } - curle = curl_easy_perform(curl); - res->rv = curl_status(curle); +leave: + req->internals = (APR_SUCCESS == rv)? internals : NULL; + return rv; +} + +static apr_status_t update_status(md_http_request_t *req) +{ + md_curl_internals_t *internals = req->internals; + long l; + apr_status_t rv = APR_SUCCESS; + + if (internals) { + rv = curl_status(curl_easy_getinfo(internals->curl, CURLINFO_RESPONSE_CODE, &l)); + if (APR_SUCCESS == rv) { + internals->response->status = (int)l; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, req->pool, + "req[%d]: http status is %d", + req->id, internals->response->status); + } + } + return rv; +} + +static void fire_status(md_http_request_t *req, apr_status_t rv) +{ + md_curl_internals_t *internals = req->internals; + + if (internals && !internals->status_fired) { + internals->status_fired = 1; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, req->pool, + "req[%d] fire callbacks", req->id); + if ((APR_SUCCESS == rv) && req->cb.on_response) { + rv = req->cb.on_response(internals->response, req->cb.on_response_data); + } - 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; + internals->rv = rv; + if (req->cb.on_status) { + req->cb.on_status(req, rv, req->cb.on_status_data); } - 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)); +} + +static apr_status_t md_curl_perform(md_http_request_t *req) +{ + apr_status_t rv = APR_SUCCESS; + CURLcode curle; + md_curl_internals_t *internals; + long l; + + if (APR_SUCCESS != (rv = internals_setup(req))) goto leave; + internals = req->internals; + + curle = curl_easy_perform(internals->curl); + + rv = curl_status(curle); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, req->pool, + "request failed(%d): %s", curle, curl_easy_strerror(curle)); + goto leave; + } + + rv = curl_status(curl_easy_getinfo(internals->curl, CURLINFO_RESPONSE_CODE, &l)); + if (APR_SUCCESS == rv) { + internals->response->status = (int)l; } + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, req->pool, "request <-- %d", + internals->response->status); + + if (req->cb.on_response) { + rv = req->cb.on_response(internals->response, req->cb.on_response_data); + req->cb.on_response = NULL; + } + +leave: + fire_status(req, rv); + md_http_req_destroy(req); + return rv; +} + +static md_http_request_t *find_curl_request(apr_array_header_t *requests, CURL *curl) +{ + md_http_request_t *req; + md_curl_internals_t *internals; + int i; - if (req->cb) { - res->rv = req->cb(res); + for (i = 0; i < requests->nelts; ++i) { + req = APR_ARRAY_IDX(requests, i, md_http_request_t*); + internals = req->internals; + if (internals && internals->curl == curl) { + return req; + } } + return NULL; +} + +static void add_to_curlm(md_http_request_t *req, CURLM *curlm) +{ + md_curl_internals_t *internals = req->internals; - rv = res->rv; + assert(curlm); + assert(internals); + if (internals->curlm == NULL) { + internals->curlm = curlm; + } + assert(internals->curlm == curlm); + curl_multi_add_handle(curlm, internals->curl); +} + +static void remove_from_curlm_and_destroy(md_http_request_t *req, CURLM *curlm) +{ + md_curl_internals_t *internals = req->internals; + + assert(curlm); + assert(internals); + assert(internals->curlm == curlm); + curl_multi_remove_handle(curlm, internals->curl); + internals->curlm = NULL; md_http_req_destroy(req); - if (req_hdrs) { - curl_slist_free_all(req_hdrs); +} + +static apr_status_t md_curl_multi_perform(md_http_t *http, apr_pool_t *p, + md_http_next_req *nextreq, void *baton) +{ + md_http_t *sub_http; + md_http_request_t *req; + CURLM *curlm = NULL; + CURLMcode mc; + struct CURLMsg *curlmsg; + apr_array_header_t *http_spares; + apr_array_header_t *requests; + int i, running, numfds, slowdown, msgcount; + apr_status_t rv; + + http_spares = apr_array_make(p, 10, sizeof(md_http_t*)); + requests = apr_array_make(p, 10, sizeof(md_http_request_t*)); + curlm = curl_multi_init(); + if (!curlm) { + rv = APR_ENOMEM; + goto leave; } + running = 1; + slowdown = 0; + while(1) { + while (1) { + /* fetch as many requests as nextreq gives us */ + if (http_spares->nelts > 0) { + sub_http = *(md_http_t **)(apr_array_pop(http_spares)); + } + else { + rv = md_http_clone(&sub_http, p, http); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, + "multi_perform[%d reqs]: setup failed", requests->nelts); + goto leave; + } + } + + rv = nextreq(&req, baton, sub_http, requests->nelts); + if (APR_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, + "multi_perform[%d reqs]: no more requests", requests->nelts); + if (!requests->nelts) { + goto leave; + } + break; + } + else if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, + "multi_perform[%d reqs]: nextreq() failed", requests->nelts); + APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http; + goto leave; + } + + if (APR_SUCCESS != (rv = internals_setup(req))) { + if (req->cb.on_status) req->cb.on_status(req, rv, req->cb.on_status_data); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, + "multi_perform[%d reqs]: setup failed", requests->nelts); + APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http; + goto leave; + } + + APR_ARRAY_PUSH(requests, md_http_request_t*) = req; + add_to_curlm(req, curlm); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, + "multi_perform[%d reqs]: added request", requests->nelts); + } + + mc = curl_multi_perform(curlm, &running); + if (CURLM_OK == mc) { + mc = curl_multi_wait(curlm, NULL, 0, 1000, &numfds); + if (numfds) slowdown = 0; + } + if (CURLM_OK != mc) { + rv = APR_ECONNABORTED; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "multi_perform[%d reqs] failed(%d): %s", + requests->nelts, mc, curl_multi_strerror(mc)); + goto leave; + } + if (!numfds) { + /* no activity on any connection, timeout */ + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, + "multi_perform[%d reqs]: slowdown %d", requests->nelts, slowdown); + if (slowdown) apr_sleep(apr_time_from_msec(100)); + ++slowdown; + } + + /* process status messages, e.g. that a request is done */ + while (running < requests->nelts) { + curlmsg = curl_multi_info_read(curlm, &msgcount); + if (!curlmsg) break; + if (curlmsg->msg == CURLMSG_DONE) { + req = find_curl_request(requests, curlmsg->easy_handle); + if (req) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, p, + "multi_perform[%d reqs]: req[%d] done", + requests->nelts, req->id); + update_status(req); + fire_status(req, curl_status(curlmsg->data.result)); + md_array_remove(requests, req); + sub_http = req->http; + APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http; + remove_from_curlm_and_destroy(req, curlm); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "multi_perform[%d reqs]: req done, but not found by handle", + requests->nelts); + } + } + } + }; + +leave: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, + "multi_perform[%d reqs]: leaving", requests->nelts); + for (i = 0; i < requests->nelts; ++i) { + req = APR_ARRAY_IDX(requests, i, md_http_request_t*); + fire_status(req, APR_SUCCESS); + sub_http = req->http; + APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http; + remove_from_curlm_and_destroy(req, curlm); + } + if (curlm) curl_multi_cleanup(curlm); return rv; } @@ -284,18 +600,48 @@ static apr_status_t md_curl_init(void) { return APR_SUCCESS; } -static void curl_req_cleanup(md_http_request_t *req) +static void md_curl_req_cleanup(md_http_request_t *req) { - if (req->internals) { - curl_easy_cleanup(req->internals); + md_curl_internals_t *internals = req->internals; + if (internals) { + if (internals->curl) { + CURL *curl = md_http_get_impl_data(req->http); + if (curl == internals->curl) { + /* NOP: we have this curl at the md_http_t already */ + } + else if (!curl) { + /* no curl at the md_http_t yet, install this one */ + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, "register curl instance at http"); + md_http_set_impl_data(req->http, internals->curl); + } + else { + /* There already is a curl at the md_http_t and it's not this one. */ + curl_easy_cleanup(internals->curl); + } + } + if (internals->req_hdrs) curl_slist_free_all(internals->req_hdrs); req->internals = NULL; } } +static void md_curl_cleanup(md_http_t *http, apr_pool_t *pool) +{ + CURL *curl; + + curl = md_http_get_impl_data(http); + if (curl) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, pool, "cleanup curl instance"); + md_http_set_impl_data(http, NULL); + curl_easy_cleanup(curl); + } +} + static md_http_impl_t impl = { md_curl_init, - curl_req_cleanup, - curl_perform + md_curl_req_cleanup, + md_curl_perform, + md_curl_multi_perform, + md_curl_cleanup, }; md_http_impl_t * md_curl_get_impl(apr_pool_t *p) diff --git a/modules/md/md_event.c b/modules/md/md_event.c new file mode 100644 index 0000000..c731d55 --- /dev/null +++ b/modules/md/md_event.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_optional.h> +#include <apr_strings.h> + +#include "md.h" +#include "md_event.h" + + +typedef struct md_subscription { + struct md_subscription *next; + md_event_cb *cb; + void *baton; +} md_subscription; + +static struct { + apr_pool_t *p; + md_subscription *subs; +} EVNT; + +static apr_status_t cleanup_setup(void *dummy) +{ + (void)dummy; + memset(&EVNT, 0, sizeof(EVNT)); + return APR_SUCCESS; +} + +void md_event_init(apr_pool_t *p) +{ + memset(&EVNT, 0, sizeof(EVNT)); + EVNT.p = p; + apr_pool_cleanup_register(p, NULL, cleanup_setup, apr_pool_cleanup_null); +} + +void md_event_subscribe(md_event_cb *cb, void *baton) +{ + md_subscription *sub; + + sub = apr_pcalloc(EVNT.p, sizeof(*sub)); + sub->cb = cb; + sub->baton = baton; + sub->next = EVNT.subs; + EVNT.subs = sub; +} + +apr_status_t md_event_raise(const char *event, + const char *mdomain, + struct md_job_t *job, + struct md_result_t *result, + apr_pool_t *p) +{ + md_subscription *sub = EVNT.subs; + apr_status_t rv; + + while (sub) { + rv = sub->cb(event, mdomain, sub->baton, job, result, p); + if (APR_SUCCESS != rv) return rv; + sub = sub->next; + } + return APR_SUCCESS; +} + +void md_event_holler(const char *event, + const char *mdomain, + struct md_job_t *job, + struct md_result_t *result, + apr_pool_t *p) +{ + md_subscription *sub = EVNT.subs; + while (sub) { + sub->cb(event, mdomain, sub->baton, job, result, p); + sub = sub->next; + } +} diff --git a/modules/md/md_event.h b/modules/md/md_event.h new file mode 100644 index 0000000..e66c3c2 --- /dev/null +++ b/modules/md/md_event.h @@ -0,0 +1,46 @@ +/* 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_event_h +#define md_event_h + +struct md_job_t; +struct md_result_t; + +typedef apr_status_t md_event_cb(const char *event, + const char *mdomain, + void *baton, + struct md_job_t *job, + struct md_result_t *result, + apr_pool_t *p); + +void md_event_init(apr_pool_t *p); + +void md_event_subscribe(md_event_cb *cb, void *baton); + +apr_status_t md_event_raise(const char *event, + const char *mdomain, + struct md_job_t *job, + struct md_result_t *result, + apr_pool_t *p); + +void md_event_holler(const char *event, + const char *mdomain, + struct md_job_t *job, + struct md_result_t *result, + apr_pool_t *p); + +#endif /* md_event_h */ diff --git a/modules/md/md_http.c b/modules/md/md_http.c index 310fc55..0d21e7b 100644 --- a/modules/md/md_http.c +++ b/modules/md/md_http.c @@ -22,14 +22,20 @@ #include "md_http.h" #include "md_log.h" +#include "md_util.h" struct md_http_t { apr_pool_t *pool; apr_bucket_alloc_t *bucket_alloc; + int next_id; apr_off_t resp_limit; md_http_impl_t *impl; + void *impl_data; /* to be used by the implementation */ const char *user_agent; const char *proxy_url; + const char *unix_socket_path; + md_http_timeouts_t timeout; + const char *ca_file; }; static md_http_impl_t *cur_impl; @@ -43,7 +49,14 @@ void md_http_use_implementation(md_http_impl_t *impl) } } -static long next_req_id; +static apr_status_t http_cleanup(void *data) +{ + md_http_t *http = data; + if (http && http->impl && http->impl->cleanup) { + http->impl->cleanup(http, http->pool); + } + return APR_SUCCESS; +} apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_agent, const char *proxy_url) @@ -74,18 +87,130 @@ apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_a if (!http->bucket_alloc) { return APR_EGENERAL; } + apr_pool_cleanup_register(p, http, http_cleanup, apr_pool_cleanup_null); *phttp = http; return APR_SUCCESS; } +apr_status_t md_http_clone(md_http_t **phttp, + apr_pool_t *p, md_http_t *source_http) +{ + apr_status_t rv; + + rv = md_http_create(phttp, p, source_http->user_agent, source_http->proxy_url); + if (APR_SUCCESS == rv) { + (*phttp)->resp_limit = source_http->resp_limit; + (*phttp)->timeout = source_http->timeout; + if (source_http->unix_socket_path) { + (*phttp)->unix_socket_path = apr_pstrdup(p, source_http->unix_socket_path); + } + if (source_http->ca_file) { + (*phttp)->ca_file = apr_pstrdup(p, source_http->ca_file); + } + } + return rv; +} + +void md_http_set_impl_data(md_http_t *http, void *data) +{ + http->impl_data = data; +} + +void *md_http_get_impl_data(md_http_t *http) +{ + return http->impl_data; +} + void md_http_set_response_limit(md_http_t *http, apr_off_t resp_limit) { http->resp_limit = resp_limit; } +void md_http_set_timeout_default(md_http_t *http, apr_time_t timeout) +{ + http->timeout.overall = timeout; +} + +void md_http_set_timeout(md_http_request_t *req, apr_time_t timeout) +{ + req->timeout.overall = timeout; +} + +void md_http_set_connect_timeout_default(md_http_t *http, apr_time_t timeout) +{ + http->timeout.connect = timeout; +} + +void md_http_set_connect_timeout(md_http_request_t *req, apr_time_t timeout) +{ + req->timeout.connect = timeout; +} + +void md_http_set_stalling_default(md_http_t *http, long bytes_per_sec, apr_time_t timeout) +{ + http->timeout.stall_bytes_per_sec = bytes_per_sec; + http->timeout.stalled = timeout; +} + +void md_http_set_stalling(md_http_request_t *req, long bytes_per_sec, apr_time_t timeout) +{ + req->timeout.stall_bytes_per_sec = bytes_per_sec; + req->timeout.stalled = timeout; +} + +void md_http_set_ca_file(md_http_t *http, const char *ca_file) +{ + http->ca_file = ca_file; +} + +void md_http_set_unix_socket_path(md_http_t *http, const char *path) +{ + http->unix_socket_path = path; +} + +static apr_status_t req_set_body(md_http_request_t *req, const char *content_type, + apr_bucket_brigade *body, apr_off_t body_len, + int detect_len) +{ + apr_status_t rv = APR_SUCCESS; + + if (body && detect_len) { + rv = apr_brigade_length(body, 1, &body_len); + if (rv != APR_SUCCESS) { + return rv; + } + } + + req->body = body; + req->body_len = body? body_len : 0; + if (content_type) { + apr_table_set(req->headers, "Content-Type", content_type); + } + else { + apr_table_unset(req->headers, "Content-Type"); + } + return rv; +} + +static apr_status_t req_set_body_data(md_http_request_t *req, const char *content_type, + const md_data_t *body) +{ + apr_bucket_brigade *bbody = NULL; + apr_status_t rv; + + if (body && body->len > 0) { + bbody = apr_brigade_create(req->pool, req->http->bucket_alloc); + rv = apr_brigade_write(bbody, NULL, NULL, body->data, body->len); + if (rv != APR_SUCCESS) { + return rv; + } + } + return req_set_body(req, content_type, bbody, body? (apr_off_t)body->len : 0, 0); +} + 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) + const char *method, const char *url, + struct apr_table_t *headers) { md_http_request_t *req; apr_pool_t *pool; @@ -95,21 +220,22 @@ static apr_status_t req_create(md_http_request_t **preq, md_http_t *http, if (rv != APR_SUCCESS) { return rv; } + apr_pool_tag(pool, "md_http_req"); req = apr_pcalloc(pool, sizeof(*req)); - req->id = next_req_id++; req->pool = pool; + req->id = http->next_id++; 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; - + req->timeout = http->timeout; + req->ca_file = http->ca_file; + req->unix_socket_path = http->unix_socket_path; *preq = req; return rv; } @@ -123,123 +249,149 @@ void md_http_req_destroy(md_http_request_t *req) 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) +void md_http_set_on_status_cb(md_http_request_t *req, md_http_status_cb *cb, void *baton) { - apr_status_t rv; - - req->body = body; - req->body_len = body? -1 : 0; + req->cb.on_status = cb; + req->cb.on_status_data = baton; +} - 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)); - } +void md_http_set_on_response_cb(md_http_request_t *req, md_http_response_cb *cb, void *baton) +{ + req->cb.on_response = cb; + req->cb.on_response_data = baton; +} + +apr_status_t md_http_perform(md_http_request_t *req) +{ + return req->http->impl->perform(req); +} + +typedef struct { + md_http_next_req *nextreq; + void *baton; +} nextreq_proxy_t; + +static apr_status_t proxy_nextreq(md_http_request_t **preq, void *baton, + md_http_t *http, int in_flight) +{ + nextreq_proxy_t *proxy = baton; - if (preq_id) { - *preq_id = req->id; - } + return proxy->nextreq(preq, proxy->baton, http, in_flight); +} + +apr_status_t md_http_multi_perform(md_http_t *http, md_http_next_req *nextreq, void *baton) +{ + nextreq_proxy_t proxy; - /* we send right away */ - rv = req->http->impl->perform(req); + proxy.nextreq = nextreq; + proxy.baton = baton; + return http->impl->multi_perform(http, http->pool, proxy_nextreq, &proxy); +} + +apr_status_t md_http_GET_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers) +{ + md_http_request_t *req; + apr_status_t rv; + rv = req_create(&req, http, "GET", url, headers); + *preq = (APR_SUCCESS == rv)? req : NULL; 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) +apr_status_t md_http_HEAD_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers) { 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); + rv = req_create(&req, http, "HEAD", url, headers); + *preq = (APR_SUCCESS == rv)? req : NULL; + return rv; } -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) +apr_status_t md_http_POST_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + struct apr_bucket_brigade *body, int detect_len) { 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; + rv = req_create(&req, http, "POST", url, headers); + if (APR_SUCCESS == rv) { + rv = req_set_body(req, content_type, body, -1, detect_len); } - - return schedule(req, NULL, 0, preq_id); + *preq = (APR_SUCCESS == rv)? req : NULL; + return rv; } -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) +apr_status_t md_http_POSTd_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + const struct md_data_t *body) { 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; + rv = req_create(&req, http, "POST", url, headers); + if (APR_SUCCESS != rv) goto cleanup; + rv = req_set_body_data(req, content_type, body); +cleanup: + if (APR_SUCCESS == rv) { + *preq = req; } - - if (content_type) { - apr_table_set(req->headers, "Content-Type", content_type); + else { + *preq = NULL; + if (req) md_http_req_destroy(req); } - return schedule(req, body, 1, preq_id); + return rv; } -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_GET_perform(struct md_http_t *http, + const char *url, struct apr_table_t *headers, + md_http_response_cb *cb, void *baton) { 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); + rv = md_http_GET_create(&req, http, url, headers); + if (APR_SUCCESS == rv) md_http_set_on_response_cb(req, cb, baton); + return (APR_SUCCESS == rv)? md_http_perform(req) : rv; } -apr_status_t md_http_await(md_http_t *http, long req_id) +apr_status_t md_http_HEAD_perform(struct md_http_t *http, + const char *url, struct apr_table_t *headers, + md_http_response_cb *cb, void *baton) { - (void)http; - (void)req_id; - return APR_SUCCESS; + md_http_request_t *req; + apr_status_t rv; + + rv = md_http_HEAD_create(&req, http, url, headers); + if (APR_SUCCESS == rv) md_http_set_on_response_cb(req, cb, baton); + return (APR_SUCCESS == rv)? md_http_perform(req) : rv; } +apr_status_t md_http_POST_perform(struct md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + apr_bucket_brigade *body, int detect_len, + md_http_response_cb *cb, void *baton) +{ + md_http_request_t *req; + apr_status_t rv; + + rv = md_http_POST_create(&req, http, url, headers, content_type, body, detect_len); + if (APR_SUCCESS == rv) md_http_set_on_response_cb(req, cb, baton); + return (APR_SUCCESS == rv)? md_http_perform(req) : rv; +} + +apr_status_t md_http_POSTd_perform(md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + const md_data_t *body, + md_http_response_cb *cb, void *baton) +{ + md_http_request_t *req; + apr_status_t rv; + + rv = md_http_POSTd_create(&req, http, url, headers, content_type, body); + if (APR_SUCCESS == rv) md_http_set_on_response_cb(req, cb, baton); + return (APR_SUCCESS == rv)? md_http_perform(req) : rv; +} diff --git a/modules/md/md_http.h b/modules/md/md_http.h index c6d94bb..2f250f6 100644 --- a/modules/md/md_http.h +++ b/modules/md/md_http.h @@ -20,35 +20,63 @@ struct apr_table_t; struct apr_bucket_brigade; struct apr_bucket_alloc_t; +struct md_data_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); +/** + * Callback invoked once per request, either when an error was encountered + * or when everything succeeded and the request is about to be released. Only + * in the last case will the status be APR_SUCCESS. + */ +typedef apr_status_t md_http_status_cb(const md_http_request_t *req, apr_status_t status, void *data); + +/** + * Callback invoked when the complete response has been received. + */ +typedef apr_status_t md_http_response_cb(const md_http_response_t *res, void *data); + +typedef struct md_http_callbacks_t md_http_callbacks_t; +struct md_http_callbacks_t { + md_http_status_cb *on_status; + void *on_status_data; + md_http_response_cb *on_response; + void *on_response_data; +}; + +typedef struct md_http_timeouts_t md_http_timeouts_t; +struct md_http_timeouts_t { + apr_time_t overall; + apr_time_t connect; + long stall_bytes_per_sec; + apr_time_t stalled; +}; struct md_http_request_t { - long id; md_http_t *http; apr_pool_t *pool; + int id; struct apr_bucket_alloc_t *bucket_alloc; const char *method; const char *url; const char *user_agent; const char *proxy_url; + const char *ca_file; + const char *unix_socket_path; 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; + md_http_timeouts_t timeout; + md_http_callbacks_t cb; 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; @@ -59,44 +87,186 @@ apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_a 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); +/** + * Clone a http instance, inheriting all settings from source_http. + * The cloned instance is not tied in any way to the source. + */ +apr_status_t md_http_clone(md_http_t **phttp, + apr_pool_t *p, md_http_t *source_http); + +/** + * Set the timeout for the complete request. This needs to take everything from + * DNS looksups, to conntects, to transfer of all data into account and should + * be sufficiently large. + * Set to 0 the have no timeout for this. + */ +void md_http_set_timeout_default(md_http_t *http, apr_time_t timeout); +void md_http_set_timeout(md_http_request_t *req, apr_time_t timeout); + +/** + * Set the timeout for establishing a connection. + * Set to 0 the have no special timeout for this. + */ +void md_http_set_connect_timeout_default(md_http_t *http, apr_time_t timeout); +void md_http_set_connect_timeout(md_http_request_t *req, apr_time_t timeout); + +/** + * Set the condition for when a transfer is considered "stalled", e.g. does not + * progress at a sufficient rate and will be aborted. + * Set to 0 the have no stall detection in place. + */ +void md_http_set_stalling_default(md_http_t *http, long bytes_per_sec, apr_time_t timeout); +void md_http_set_stalling(md_http_request_t *req, long bytes_per_sec, apr_time_t timeout); + +/** + * Set a CA file (in PERM format) to use for root certificates when + * verifying SSL connections. If not set (or set to NULL), the systems + * certificate store will be used. + */ +void md_http_set_ca_file(md_http_t *http, const char *ca_file); + +/** + * Set the path of a unix domain socket for use instead of TCP + * in a connection. Disable by providing NULL as path. + */ +void md_http_set_unix_socket_path(md_http_t *http, const char *path); + +/** + * Perform the request. Then this function returns, the request and + * all its memory has been freed and must no longer be used. + */ +apr_status_t md_http_perform(md_http_request_t *request); -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); +/** + * Set the callback to be invoked once the status of a request is known. + * @param req the request + * @param cb the callback to invoke on the response + * @param baton data passed to the callback + */ +void md_http_set_on_status_cb(md_http_request_t *req, md_http_status_cb *cb, void *baton); + +/** + * Set the callback to be invoked when the complete + * response has been successfully received. The HTTP status may + * be 500, however. + * @param req the request + * @param cb the callback to invoke on the response + * @param baton data passed to the callback + */ +void md_http_set_on_response_cb(md_http_request_t *req, md_http_response_cb *cb, void *baton); + +/** + * Create a GET request. + * @param preq the created request after success + * @param http the md_http instance + * @param url the url to GET + * @param headers request headers + */ +apr_status_t md_http_GET_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers); + +/** + * Create a HEAD request. + * @param preq the created request after success + * @param http the md_http instance + * @param url the url to GET + * @param headers request headers + */ +apr_status_t md_http_HEAD_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers); -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); +/** + * Create a POST request with a bucket brigade as request body. + * @param preq the created request after success + * @param http the md_http instance + * @param url the url to GET + * @param headers request headers + * @param content_type the content_type of the body or NULL + * @param body the body of the request or NULL + * @param detect_len scan the body to detect its length + */ +apr_status_t md_http_POST_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + struct apr_bucket_brigade *body, int detect_len); -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); +/** + * Create a POST request with known request body data. + * @param preq the created request after success + * @param http the md_http instance + * @param url the url to GET + * @param headers request headers + * @param content_type the content_type of the body or NULL + * @param body the body of the request or NULL + */ +apr_status_t md_http_POSTd_create(md_http_request_t **preq, md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + const struct md_data_t *body); -apr_status_t md_http_await(md_http_t *http, long req_id); +/* + * Convenience functions for create+perform. + */ +apr_status_t md_http_GET_perform(md_http_t *http, const char *url, + struct apr_table_t *headers, + md_http_response_cb *cb, void *baton); +apr_status_t md_http_HEAD_perform(md_http_t *http, const char *url, + struct apr_table_t *headers, + md_http_response_cb *cb, void *baton); +apr_status_t md_http_POST_perform(md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + struct apr_bucket_brigade *body, int detect_len, + md_http_response_cb *cb, void *baton); +apr_status_t md_http_POSTd_perform(md_http_t *http, const char *url, + struct apr_table_t *headers, const char *content_type, + const struct md_data_t *body, + md_http_response_cb *cb, void *baton); void md_http_req_destroy(md_http_request_t *req); +/** Return the next request for processing on APR_SUCCESS. Return ARP_ENOENT + * when no request is available. Anything else is an error. + */ +typedef apr_status_t md_http_next_req(md_http_request_t **preq, void *baton, + md_http_t *http, int in_flight); + +/** + * Perform requests in parallel as retrieved from the nextreq function. + * There are as many requests in flight as the nextreq functions provides. + * + * To limit the number of parallel requests, nextreq should return APR_ENOENT when the limit + * is reached. It will be called again when the number of in_flight requests changes. + * + * When all requests are done, nextreq will be called one more time. Should it not + * return anything, this function returns. + */ +apr_status_t md_http_multi_perform(md_http_t *http, md_http_next_req *nextreq, void *baton); + /**************************************************************************************************/ /* interface to implementation */ typedef apr_status_t md_http_init_cb(void); +typedef void md_http_cleanup_cb(md_http_t *req, apr_pool_t *p); 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 apr_status_t md_http_multi_perform_cb(md_http_t *http, apr_pool_t *p, + md_http_next_req *nextreq, void *baton); 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; + md_http_multi_perform_cb *multi_perform; + md_http_cleanup_cb *cleanup; }; void md_http_use_implementation(md_http_impl_t *impl); +/** + * get/set data the implementation wants to remember between requests + * in the same md_http_t instance. + */ +void md_http_set_impl_data(md_http_t *http, void *data); +void *md_http_get_impl_data(md_http_t *http); #endif /* md_http_h */ diff --git a/modules/md/md_json.c b/modules/md/md_json.c index f73ab14..e0f977e 100644 --- a/modules/md/md_json.c +++ b/modules/md/md_json.c @@ -18,10 +18,12 @@ #include <apr_lib.h> #include <apr_strings.h> #include <apr_buckets.h> +#include <apr_date.h> #include "md_json.h" #include "md_log.h" #include "md_http.h" +#include "md_time.h" #include "md_util.h" /* jansson thinks everyone compiles with the platform's cc in its fullest capabilities @@ -106,12 +108,12 @@ 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_copy(apr_pool_t *pool, const 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) +md_json_t *md_json_clone(apr_pool_t *pool, const md_json_t *json) { return json_create(pool, json_deep_copy(json->j)); } @@ -120,7 +122,7 @@ md_json_t *md_json_clone(apr_pool_t *pool, md_json_t *json) /* selectors */ -static json_t *jselect(md_json_t *json, va_list ap) +static json_t *jselect(const md_json_t *json, va_list ap) { json_t *j; const char *key; @@ -168,6 +170,31 @@ static apr_status_t jselect_add(json_t *val, md_json_t *json, va_list ap) j = jselect_parent(&key, 1, json, ap); if (!j || !json_is_object(j)) { + 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)) { + return APR_EINVAL; + } + + json_array_append(aj, val); + return APR_SUCCESS; +} + +static apr_status_t jselect_insert(json_t *val, size_t index, 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; } @@ -183,7 +210,12 @@ static apr_status_t jselect_add(json_t *val, md_json_t *json, va_list ap) return APR_EINVAL; } - json_array_append(aj, val); + if (json_array_size(aj) <= index) { + json_array_append(aj, val); + } + else { + json_array_insert(aj, index, val); + } return APR_SUCCESS; } @@ -195,13 +227,11 @@ static apr_status_t jselect_set(json_t *val, md_json_t *json, va_list ap) 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); @@ -246,7 +276,7 @@ static apr_status_t jselect_set_new(json_t *val, md_json_t *json, va_list ap) return APR_SUCCESS; } -int md_json_has_key(md_json_t *json, ...) +int md_json_has_key(const md_json_t *json, ...) { json_t *j; va_list ap; @@ -259,9 +289,45 @@ int md_json_has_key(md_json_t *json, ...) } /**************************************************************************************************/ +/* type things */ + +int md_json_is(const md_json_type_t jtype, md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + switch (jtype) { + case MD_JSON_TYPE_OBJECT: return (j && json_is_object(j)); + case MD_JSON_TYPE_ARRAY: return (j && json_is_array(j)); + case MD_JSON_TYPE_STRING: return (j && json_is_string(j)); + case MD_JSON_TYPE_REAL: return (j && json_is_real(j)); + case MD_JSON_TYPE_INT: return (j && json_is_integer(j)); + case MD_JSON_TYPE_BOOL: return (j && (json_is_true(j) || json_is_false(j))); + case MD_JSON_TYPE_NULL: return (j == NULL); + } + return 0; +} + +static const char *md_json_type_name(const md_json_t *json) +{ + json_t *j = json->j; + if (json_is_object(j)) return "object"; + if (json_is_array(j)) return "array"; + if (json_is_string(j)) return "string"; + if (json_is_real(j)) return "real"; + if (json_is_integer(j)) return "integer"; + if (json_is_true(j)) return "true"; + if (json_is_false(j)) return "false"; + return "unknown"; +} + +/**************************************************************************************************/ /* booleans */ -int md_json_getb(md_json_t *json, ...) +int md_json_getb(const md_json_t *json, ...) { json_t *j; va_list ap; @@ -287,7 +353,7 @@ apr_status_t md_json_setb(int value, md_json_t *json, ...) /**************************************************************************************************/ /* numbers */ -double md_json_getn(md_json_t *json, ...) +double md_json_getn(const md_json_t *json, ...) { json_t *j; va_list ap; @@ -312,7 +378,7 @@ apr_status_t md_json_setn(double value, md_json_t *json, ...) /**************************************************************************************************/ /* longs */ -long md_json_getl(md_json_t *json, ...) +long md_json_getl(const md_json_t *json, ...) { json_t *j; va_list ap; @@ -337,7 +403,7 @@ apr_status_t md_json_setl(long value, md_json_t *json, ...) /**************************************************************************************************/ /* strings */ -const char *md_json_gets(md_json_t *json, ...) +const char *md_json_gets(const md_json_t *json, ...) { json_t *j; va_list ap; @@ -349,7 +415,7 @@ const char *md_json_gets(md_json_t *json, ...) return (j && json_is_string(j))? json_string_value(j) : NULL; } -const char *md_json_dups(apr_pool_t *p, md_json_t *json, ...) +const char *md_json_dups(apr_pool_t *p, const md_json_t *json, ...) { json_t *j; va_list ap; @@ -373,6 +439,35 @@ apr_status_t md_json_sets(const char *value, md_json_t *json, ...) } /**************************************************************************************************/ +/* time */ + +apr_time_t md_json_get_time(const 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_string(j)) return 0; + return apr_date_parse_rfc(json_string_value(j)); +} + +apr_status_t md_json_set_time(apr_time_t value, md_json_t *json, ...) +{ + char ts[APR_RFC822_DATE_LEN]; + va_list ap; + apr_status_t rv; + + apr_rfc822_date(ts, value); + va_start(ap, json); + rv = jselect_set_new(json_string(ts), json, ap); + va_end(ap); + return rv; +} + +/**************************************************************************************************/ /* json itself */ md_json_t *md_json_getj(md_json_t *json, ...) @@ -394,7 +489,42 @@ md_json_t *md_json_getj(md_json_t *json, ...) return NULL; } -apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...) +md_json_t *md_json_dupj(apr_pool_t *p, const md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (j) { + json_incref(j); + return json_create(p, j); + } + return NULL; +} + +const md_json_t *md_json_getcj(const 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(const md_json_t *value, md_json_t *json, ...) { va_list ap; apr_status_t rv; @@ -422,7 +552,7 @@ apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...) return rv; } -apr_status_t md_json_addj(md_json_t *value, md_json_t *json, ...) +apr_status_t md_json_addj(const md_json_t *value, md_json_t *json, ...) { va_list ap; apr_status_t rv; @@ -433,6 +563,36 @@ apr_status_t md_json_addj(md_json_t *value, md_json_t *json, ...) return rv; } +apr_status_t md_json_insertj(md_json_t *value, size_t index, md_json_t *json, ...) +{ + va_list ap; + apr_status_t rv; + + va_start(ap, json); + rv = jselect_insert(value->j, index, json, ap); + va_end(ap); + return rv; +} + +apr_size_t md_json_limita(size_t max_elements, md_json_t *json, ...) +{ + json_t *j; + va_list ap; + apr_size_t n = 0; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (j && json_is_array(j)) { + n = json_array_size(j); + while (n > max_elements) { + json_array_remove(j, n-1); + n = json_array_size(j); + } + } + return n; +} /**************************************************************************************************/ /* arrays / objects */ @@ -474,7 +634,7 @@ apr_status_t md_json_del(md_json_t *json, ...) /**************************************************************************************************/ /* object strings */ -apr_status_t md_json_gets_dict(apr_table_t *dict, md_json_t *json, ...) +apr_status_t md_json_gets_dict(apr_table_t *dict, const md_json_t *json, ...) { json_t *j; va_list ap; @@ -557,7 +717,7 @@ apr_status_t md_json_clone_to(void *value, md_json_t *json, apr_pool_t *p, void 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) +apr_status_t md_json_clone_from(void **pvalue, const md_json_t *json, apr_pool_t *p, void *baton) { (void)baton; *pvalue = md_json_clone(p, json); @@ -568,7 +728,7 @@ apr_status_t md_json_clone_from(void **pvalue, md_json_t *json, apr_pool_t *p, v /* array generic */ apr_status_t md_json_geta(apr_array_header_t *a, md_json_from_cb *cb, void *baton, - md_json_t *json, ...) + const md_json_t *json, ...) { json_t *j; va_list ap; @@ -672,10 +832,36 @@ int md_json_itera(md_json_itera_cb *cb, void *baton, md_json_t *json, ...) return 1; } +int md_json_iterkey(md_json_iterkey_cb *cb, void *baton, md_json_t *json, ...) +{ + json_t *j; + va_list ap; + const char *key; + json_t *val; + md_json_t wrap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (!j || !json_is_object(j)) { + return 0; + } + + wrap.p = json->p; + json_object_foreach(j, key, val) { + wrap.j = val; + if (!cb(baton, key, &wrap)) { + return 0; + } + } + return 1; +} + /**************************************************************************************************/ /* array strings */ -apr_status_t md_json_getsa(apr_array_header_t *a, md_json_t *json, ...) +apr_status_t md_json_getsa(apr_array_header_t *a, const md_json_t *json, ...) { json_t *j; va_list ap; @@ -711,6 +897,7 @@ apr_status_t md_json_dupsa(apr_array_header_t *a, apr_pool_t *p, md_json_t *json size_t index; json_t *val; + apr_array_clear(a); json_array_foreach(j, index, val) { if (json_is_string(val)) { APR_ARRAY_PUSH(a, const char *) = apr_pstrdup(p, json_string_value(val)); @@ -757,7 +944,7 @@ apr_status_t md_json_setsa(apr_array_header_t *a, md_json_t *json, ...) /* formatting, parsing */ typedef struct { - md_json_t *json; + const md_json_t *json; md_json_fmt_t fmt; const char *fname; apr_file_t *f; @@ -782,7 +969,7 @@ static int dump_cb(const char *buffer, size_t len, void *baton) 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) +apr_status_t md_json_writeb(const 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; @@ -791,22 +978,25 @@ apr_status_t md_json_writeb(md_json_t *json, md_json_fmt_t fmt, apr_bucket_briga 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); + char *chunk; - memcpy(chunk, buffer, len); - APR_ARRAY_PUSH(chunks, const char *) = chunk; + if (len > 0) { + chunk = apr_palloc(chunks->pool, len+1); + memcpy(chunk, buffer, len); + chunk[len] = '\0'; + 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) +const char *md_json_writep(const 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) { + if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "md_json_writep failed to dump JSON"); return NULL; @@ -816,33 +1006,32 @@ const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt) case 0: return ""; case 1: - return APR_ARRAY_IDX(chunks, 0, const char *); + 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 md_json_writef(const 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) { + if ((s = md_json_writep(json, p, fmt))) { rv = apr_file_write_full(f, s, strlen(s), NULL); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, json->p, "md_json_writef: error writing file"); + } } else { rv = APR_EINVAL; - } - - if (APR_SUCCESS != rv) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, json->p, "md_json_writef"); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, json->p, + "md_json_writef: error dumping json (%s)", md_json_dump_state(json, p)); } return rv; } -apr_status_t md_json_fcreatex(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, +apr_status_t md_json_fcreatex(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, const char *fpath, apr_fileperms_t perms) { apr_status_t rv; @@ -866,7 +1055,7 @@ static apr_status_t write_json(void *baton, apr_file_t *f, apr_pool_t *p) return rv; } -apr_status_t md_json_freplace(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, +apr_status_t md_json_freplace(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, const char *fpath, apr_fileperms_t perms) { j_write_ctx ctx; @@ -938,11 +1127,14 @@ apr_status_t md_json_readb(md_json_t **pjson, apr_pool_t *pool, apr_bucket_briga json_t *j; j = json_load_callback(load_cb, bb, 0, &error); - if (!j) { - return APR_EINVAL; + if (j) { + *pjson = json_create(pool, j); + } else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, pool, + "failed to load JSON file: %s (line %d:%d)", + error.text, error.line, error.column); } - *pjson = json_create(pool, j); - return APR_SUCCESS; + return (j && *pjson) ? APR_SUCCESS : APR_EINVAL; } static size_t load_file_cb(void *data, size_t max_len, void *baton) @@ -993,12 +1185,18 @@ apr_status_t md_json_readf(md_json_t **pjson, apr_pool_t *p, const char *fpath) 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); - } + const char *ctype, *p; + + *pjson = NULL; + if (!res->body) goto cleanup; + ctype = md_util_parse_ct(res->req->pool, apr_table_get(res->headers, "content-type")); + if (!ctype) goto cleanup; + p = ctype + strlen(ctype) +1; + if (!strcmp(p - sizeof("/json"), "/json") + || !strcmp(p - sizeof("+json"), "+json")) { + rv = md_json_readb(pjson, pool, res->body); } +cleanup: return rv; } @@ -1008,26 +1206,24 @@ typedef struct { md_json_t *json; } resp_data; -static apr_status_t json_resp_cb(const md_http_response_t *res) +static apr_status_t json_resp_cb(const md_http_response_t *res, void *data) { - resp_data *resp = res->req->baton; + resp_data *resp = data; 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); + rv = md_http_GET_perform(http, url, NULL, json_resp_cb, &resp); if (rv == APR_SUCCESS) { - md_http_await(http, req_id); *pjson = resp.json; return resp.rv; } @@ -1035,3 +1231,81 @@ apr_status_t md_json_http_get(md_json_t **pjson, apr_pool_t *pool, return rv; } + +apr_status_t md_json_copy_to(md_json_t *dest, const md_json_t *src, ...) +{ + json_t *j; + va_list ap; + apr_status_t rv = APR_SUCCESS; + + va_start(ap, src); + j = jselect(src, ap); + va_end(ap); + + if (j) { + va_start(ap, src); + rv = jselect_set(j, dest, ap); + va_end(ap); + } + return rv; +} + +const char *md_json_dump_state(const md_json_t *json, apr_pool_t *p) +{ + if (!json) return "NULL"; + return apr_psprintf(p, "%s, refc=%ld", md_json_type_name(json), (long)json->j->refcount); +} + +apr_status_t md_json_set_timeperiod(const md_timeperiod_t *tp, md_json_t *json, ...) +{ + char ts[APR_RFC822_DATE_LEN]; + json_t *jn, *j; + va_list ap; + const char *key; + apr_status_t rv; + + if (tp && tp->start && tp->end) { + jn = json_object(); + apr_rfc822_date(ts, tp->start); + json_object_set_new(jn, "from", json_string(ts)); + apr_rfc822_date(ts, tp->end); + json_object_set_new(jn, "until", json_string(ts)); + + va_start(ap, json); + rv = jselect_set_new(jn, json, ap); + va_end(ap); + return rv; + } + else { + 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; + } +} + +apr_status_t md_json_get_timeperiod(md_timeperiod_t *tp, md_json_t *json, ...) +{ + json_t *j, *jts; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + memset(tp, 0, sizeof(*tp)); + if (!j) goto not_found; + jts = json_object_get(j, "from"); + if (!jts || !json_is_string(jts)) goto not_found; + tp->start = apr_date_parse_rfc(json_string_value(jts)); + jts = json_object_get(j, "until"); + if (!jts || !json_is_string(jts)) goto not_found; + tp->end = apr_date_parse_rfc(json_string_value(jts)); + return APR_SUCCESS; +not_found: + return APR_ENOENT; +} diff --git a/modules/md/md_json.h b/modules/md/md_json.h index 7f2e4f3..50b8828 100644 --- a/modules/md/md_json.h +++ b/modules/md/md_json.h @@ -24,11 +24,22 @@ struct apr_file_t; struct md_http_t; struct md_http_response_t; - +struct md_timeperiod_t; typedef struct md_json_t md_json_t; typedef enum { + MD_JSON_TYPE_OBJECT, + MD_JSON_TYPE_ARRAY, + MD_JSON_TYPE_STRING, + MD_JSON_TYPE_REAL, + MD_JSON_TYPE_INT, + MD_JSON_TYPE_BOOL, + MD_JSON_TYPE_NULL, +} md_json_type_t; + + +typedef enum { MD_JSON_FMT_COMPACT, MD_JSON_FMT_INDENT, } md_json_fmt_t; @@ -36,38 +47,50 @@ typedef enum { 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); +md_json_t *md_json_copy(apr_pool_t *pool, const md_json_t *json); +md_json_t *md_json_clone(apr_pool_t *pool, const md_json_t *json); + -int md_json_has_key(md_json_t *json, ...); +int md_json_has_key(const md_json_t *json, ...); +int md_json_is(const md_json_type_t type, md_json_t *json, ...); /* boolean manipulation */ -int md_json_getb(md_json_t *json, ...); +int md_json_getb(const 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, ...); +double md_json_getn(const 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, ...); +long md_json_getl(const 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, ...); +const char *md_json_gets(const md_json_t *json, ...); +const char *md_json_dups(apr_pool_t *p, const md_json_t *json, ...); apr_status_t md_json_sets(const char *s, md_json_t *json, ...); +/* timestamp manipulation */ +apr_time_t md_json_get_time(const md_json_t *json, ...); +apr_status_t md_json_set_time(apr_time_t value, 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, ...); +md_json_t *md_json_dupj(apr_pool_t *p, const md_json_t *json, ...); +const md_json_t *md_json_getcj(const md_json_t *json, ...); +apr_status_t md_json_setj(const md_json_t *value, md_json_t *json, ...); +apr_status_t md_json_addj(const md_json_t *value, md_json_t *json, ...); +apr_status_t md_json_insertj(md_json_t *value, size_t index, 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, ...); +/* Remove all array elements beyond max_elements */ +apr_size_t md_json_limita(size_t max_elements, 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); @@ -78,34 +101,39 @@ apr_status_t md_json_pass_from(void **pvalue, md_json_t *json, apr_pool_t *p, vo /* 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); +apr_status_t md_json_clone_from(void **pvalue, const 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, ...); + void *baton, const 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, ...); +/* Called on each array element, aborts iteration when returning 0 */ 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, ...); +/* Called on each object key, aborts iteration when returning 0 */ +typedef int md_json_iterkey_cb(void *baton, const char* key, md_json_t *json); +int md_json_iterkey(md_json_iterkey_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_gets_dict(apr_table_t *dict, const 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_getsa(apr_array_header_t *a, const 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, +apr_status_t md_json_writeb(const md_json_t *json, md_json_fmt_t fmt, struct apr_bucket_brigade *bb); +const char *md_json_writep(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt); +apr_status_t md_json_writef(const 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, +apr_status_t md_json_fcreatex(const 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, +apr_status_t md_json_freplace(const 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); @@ -119,4 +147,11 @@ apr_status_t md_json_http_get(md_json_t **pjson, apr_pool_t *pool, apr_status_t md_json_read_http(md_json_t **pjson, apr_pool_t *pool, const struct md_http_response_t *res); +apr_status_t md_json_copy_to(md_json_t *dest, const md_json_t *src, ...); + +const char *md_json_dump_state(const md_json_t *json, apr_pool_t *p); + +apr_status_t md_json_set_timeperiod(const struct md_timeperiod_t *tp, md_json_t *json, ...); +apr_status_t md_json_get_timeperiod(struct md_timeperiod_t *tp, md_json_t *json, ...); + #endif /* md_json_h */ diff --git a/modules/md/md_jws.c b/modules/md/md_jws.c index 37c1b0e..c0e8c1b 100644 --- a/modules/md/md_jws.c +++ b/modules/md/md_jws.c @@ -25,65 +25,67 @@ #include "md_log.h" #include "md_util.h" -static int header_set(void *data, const char *key, const char *val) +apr_status_t md_jws_get_jwk(md_json_t **pjwk, apr_pool_t *p, struct md_pkey_t *pkey) { - md_json_sets(val, (md_json_t *)data, key, NULL); - return 1; + md_json_t *jwk; + + if (!pkey) return APR_EINVAL; + + jwk = md_json_create(p); + md_json_sets(md_pkey_get_rsa_e64(pkey, p), jwk, "e", NULL); + md_json_sets("RSA", jwk, "kty", NULL); + md_json_sets(md_pkey_get_rsa_n64(pkey, p), jwk, "n", NULL); + *pjwk = jwk; + return APR_SUCCESS; } 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, + md_data_t *payload, md_json_t *prot_fields, struct md_pkey_t *pkey, const char *key_id) { - md_json_t *msg, *jprotected; + md_json_t *msg, *jprotected, *jwk; const char *prot64, *pay64, *sign64, *sign, *prot; - apr_status_t rv = APR_SUCCESS; + md_data_t data; + apr_status_t rv; - *pmsg = NULL; - msg = md_json_create(p); - - jprotected = md_json_create(p); + jprotected = md_json_clone(p, prot_fields); 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); + rv = md_jws_get_jwk(&jwk, p, pkey); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "get jwk"); + goto cleanup; + } + md_json_setj(jwk, jprotected, "jwk", 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!>"); + prot = md_json_writep(jprotected, p, MD_JSON_FMT_COMPACT); if (!prot) { rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "serialize protected"); + goto cleanup; } - - 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); + md_data_init(&data, prot, strlen(prot)); + prot64 = md_util_base64url_encode(&data, p); + md_json_sets(prot64, msg, "protected", NULL); - rv = md_crypt_sign64(&sign64, pkey, p, sign, strlen(sign)); - } + pay64 = md_util_base64url_encode(payload, p); + md_json_sets(pay64, msg, "payload", NULL); + sign = apr_psprintf(p, "%s.%s", prot64, pay64); - 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 { + rv = md_crypt_sign64(&sign64, pkey, p, sign, strlen(sign)); + if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "jwk signed message"); - } - + goto cleanup; + } + md_json_sets(sign64, msg, "signature", NULL); + +cleanup: *pmsg = (APR_SUCCESS == rv)? msg : NULL; return rv; } @@ -91,6 +93,7 @@ apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p, apr_status_t md_jws_pkey_thumb(const char **pthumb, apr_pool_t *p, struct md_pkey_t *pkey) { const char *e64, *n64, *s; + md_data_t data; apr_status_t rv; e64 = md_pkey_get_rsa_e64(pkey, p); @@ -101,6 +104,45 @@ apr_status_t md_jws_pkey_thumb(const char **pthumb, apr_pool_t *p, struct md_pke /* 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)); + md_data_init_str(&data, s); + rv = md_crypt_sha256_digest64(pthumb, p, &data); + return rv; +} + +apr_status_t md_jws_hmac(md_json_t **pmsg, apr_pool_t *p, + md_data_t *payload, md_json_t *prot_fields, + const md_data_t *hmac_key) +{ + md_json_t *msg, *jprotected; + const char *prot64, *pay64, *mac64, *sign, *prot; + md_data_t data; + apr_status_t rv; + + msg = md_json_create(p); + jprotected = md_json_clone(p, prot_fields); + md_json_sets("HS256", jprotected, "alg", NULL); + prot = md_json_writep(jprotected, p, MD_JSON_FMT_COMPACT); + if (!prot) { + rv = APR_EINVAL; + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "serialize protected"); + goto cleanup; + } + + md_data_init(&data, prot, strlen(prot)); + prot64 = md_util_base64url_encode(&data, p); + md_json_sets(prot64, msg, "protected", NULL); + + pay64 = md_util_base64url_encode(payload, p); + md_json_sets(pay64, msg, "payload", NULL); + sign = apr_psprintf(p, "%s.%s", prot64, pay64); + + rv = md_crypt_hmac64(&mac64, hmac_key, p, sign, strlen(sign)); + if (APR_SUCCESS != rv) { + goto cleanup; + } + md_json_sets(mac64, msg, "signature", NULL); + +cleanup: + *pmsg = (APR_SUCCESS == rv)? msg : NULL; return rv; } diff --git a/modules/md/md_jws.h b/modules/md/md_jws.h index e7c145e..466f2df 100644 --- a/modules/md/md_jws.h +++ b/modules/md/md_jws.h @@ -20,11 +20,33 @@ struct apr_table_t; struct md_json_t; struct md_pkey_t; +struct md_data_t; +/** + * Get the JSON value of the 'jwk' field for the given key. + */ +apr_status_t md_jws_get_jwk(md_json_t **pjwk, apr_pool_t *p, struct md_pkey_t *pkey); + +/** + * Get the JWS key signed JSON message with given payload and protected fields, signed + * using the given key and optional key_id. + */ 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_data_t *payload, md_json_t *prot_fields, struct md_pkey_t *pkey, const char *key_id); +/** + * Get the 'Thumbprint' as defined in RFC8555 for the given key in + * base64 encoding. + */ +apr_status_t md_jws_pkey_thumb(const char **pthumb64, apr_pool_t *p, struct md_pkey_t *pkey); + +/** + * Get the JWS HS256 signed message for given payload and protected fields, + * using the base64 encoded MAC key. + */ +apr_status_t md_jws_hmac(md_json_t **pmsg, apr_pool_t *p, + struct md_data_t *payload, md_json_t *prot_fields, + const struct md_data_t *hmac_key); -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.h b/modules/md/md_log.h index 73885f2..19e688f 100644 --- a/modules/md/md_log.h +++ b/modules/md/md_log.h @@ -38,6 +38,10 @@ typedef enum { #define MD_LOG_MARK __FILE__,__LINE__ +#ifndef APLOGNO +#define APLOGNO(n) "AH" #n ": " +#endif + 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); diff --git a/modules/md/md_ocsp.c b/modules/md/md_ocsp.c new file mode 100644 index 0000000..8cbf05b --- /dev/null +++ b/modules/md/md_ocsp.c @@ -0,0 +1,1063 @@ +/* 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_hash.h> +#include <apr_time.h> +#include <apr_date.h> +#include <apr_strings.h> +#include <apr_thread_mutex.h> + +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/ocsp.h> +#include <openssl/pem.h> +#include <openssl/x509v3.h> + +#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 + +#include "md.h" +#include "md_crypt.h" +#include "md_event.h" +#include "md_json.h" +#include "md_log.h" +#include "md_http.h" +#include "md_json.h" +#include "md_result.h" +#include "md_status.h" +#include "md_store.h" +#include "md_util.h" +#include "md_ocsp.h" + +#define MD_OCSP_ID_LENGTH SHA_DIGEST_LENGTH + +struct md_ocsp_reg_t { + apr_pool_t *p; + md_store_t *store; + const char *user_agent; + const char *proxy_url; + apr_hash_t *id_by_external_id; + apr_hash_t *ostat_by_id; + apr_thread_mutex_t *mutex; + md_timeslice_t renew_window; + md_job_notify_cb *notify; + void *notify_ctx; + apr_time_t min_delay; +}; + +typedef struct md_ocsp_status_t md_ocsp_status_t; +struct md_ocsp_status_t { + md_data_t id; + const char *hexid; + const char *hex_sha256; + OCSP_CERTID *certid; + const char *responder_url; + + apr_time_t next_run; /* when the responder shall be asked again */ + int errors; /* consecutive failed attempts */ + + md_ocsp_cert_stat_t resp_stat; + md_data_t resp_der; + md_timeperiod_t resp_valid; + + md_data_t req_der; + OCSP_REQUEST *ocsp_req; + md_ocsp_reg_t *reg; + + const char *md_name; + const char *file_name; + + apr_time_t resp_mtime; + apr_time_t resp_last_check; +}; + +typedef struct md_ocsp_id_map_t md_ocsp_id_map_t; +struct md_ocsp_id_map_t { + md_data_t id; + md_data_t external_id; +}; + +static void md_openssl_free(void *d) +{ + OPENSSL_free(d); +} + +const char *md_ocsp_cert_stat_name(md_ocsp_cert_stat_t stat) +{ + switch (stat) { + case MD_OCSP_CERT_ST_GOOD: return "good"; + case MD_OCSP_CERT_ST_REVOKED: return "revoked"; + default: return "unknown"; + } +} + +md_ocsp_cert_stat_t md_ocsp_cert_stat_value(const char *name) +{ + if (name && !strcmp("good", name)) return MD_OCSP_CERT_ST_GOOD; + if (name && !strcmp("revoked", name)) return MD_OCSP_CERT_ST_REVOKED; + return MD_OCSP_CERT_ST_UNKNOWN; +} + +apr_status_t md_ocsp_init_id(md_data_t *id, apr_pool_t *p, const md_cert_t *cert) +{ + unsigned char iddata[SHA_DIGEST_LENGTH]; + X509 *x = md_cert_get_X509(cert); + unsigned int ulen = 0; + + md_data_null(id); + if (X509_digest(x, EVP_sha1(), iddata, &ulen) != 1) { + return APR_EGENERAL; + } + md_data_assign_pcopy(id, (const char*)iddata, ulen, p); + return APR_SUCCESS; +} + +static void ostat_req_cleanup(md_ocsp_status_t *ostat) +{ + if (ostat->ocsp_req) { + OCSP_REQUEST_free(ostat->ocsp_req); + ostat->ocsp_req = NULL; + } + md_data_clear(&ostat->req_der); +} + +static int ostat_cleanup(void *ctx, const void *key, apr_ssize_t klen, const void *val) +{ + md_ocsp_reg_t *reg = ctx; + md_ocsp_status_t *ostat = (md_ocsp_status_t *)val; + + (void)reg; + (void)key; + (void)klen; + ostat_req_cleanup(ostat); + if (ostat->certid) { + OCSP_CERTID_free(ostat->certid); + ostat->certid = NULL; + } + md_data_clear(&ostat->resp_der); + return 1; +} + +static int ostat_should_renew(md_ocsp_status_t *ostat) +{ + md_timeperiod_t renewal; + + renewal = md_timeperiod_slice_before_end(&ostat->resp_valid, &ostat->reg->renew_window); + return md_timeperiod_has_started(&renewal, apr_time_now()); +} + +static apr_status_t ostat_set(md_ocsp_status_t *ostat, md_ocsp_cert_stat_t stat, + md_data_t *der, md_timeperiod_t *valid, apr_time_t mtime) +{ + apr_status_t rv; + + rv = md_data_assign_copy(&ostat->resp_der, der->data, der->len); + if (APR_SUCCESS != rv) goto cleanup; + + ostat->resp_stat = stat; + ostat->resp_valid = *valid; + ostat->resp_mtime = mtime; + + ostat->errors = 0; + ostat->next_run = md_timeperiod_slice_before_end( + &ostat->resp_valid, &ostat->reg->renew_window).start; + +cleanup: + return rv; +} + +static apr_status_t ostat_from_json(md_ocsp_cert_stat_t *pstat, + md_data_t *resp_der, md_timeperiod_t *resp_valid, + md_json_t *json, apr_pool_t *p) +{ + const char *s; + md_timeperiod_t valid; + apr_status_t rv = APR_ENOENT; + + memset(resp_der, 0, sizeof(*resp_der)); + memset(resp_valid, 0, sizeof(*resp_valid)); + s = md_json_dups(p, json, MD_KEY_VALID, MD_KEY_FROM, NULL); + if (s && *s) valid.start = apr_date_parse_rfc(s); + s = md_json_dups(p, json, MD_KEY_VALID, MD_KEY_UNTIL, NULL); + if (s && *s) valid.end = apr_date_parse_rfc(s); + s = md_json_dups(p, json, MD_KEY_RESPONSE, NULL); + if (!s || !*s) goto cleanup; + md_util_base64url_decode(resp_der, s, p); + *pstat = md_ocsp_cert_stat_value(md_json_gets(json, MD_KEY_STATUS, NULL)); + *resp_valid = valid; + rv = APR_SUCCESS; +cleanup: + return rv; +} + +static void ostat_to_json(md_json_t *json, md_ocsp_cert_stat_t stat, + const md_data_t *resp_der, const md_timeperiod_t *resp_valid, + apr_pool_t *p) +{ + const char *s = NULL; + + if (resp_der->len > 0) { + md_json_sets(md_util_base64url_encode(resp_der, p), json, MD_KEY_RESPONSE, NULL); + s = md_ocsp_cert_stat_name(stat); + if (s) md_json_sets(s, json, MD_KEY_STATUS, NULL); + md_json_set_timeperiod(resp_valid, json, MD_KEY_VALID, NULL); + } +} + +static apr_status_t ocsp_status_refresh(md_ocsp_status_t *ostat, apr_pool_t *ptemp) +{ + md_store_t *store = ostat->reg->store; + md_json_t *jprops; + apr_time_t mtime; + apr_status_t rv = APR_EAGAIN; + md_data_t resp_der; + md_timeperiod_t resp_valid; + md_ocsp_cert_stat_t resp_stat; + /* Check if the store holds a newer response than the one we have */ + mtime = md_store_get_modified(store, MD_SG_OCSP, ostat->md_name, ostat->file_name, ptemp); + if (mtime <= ostat->resp_mtime) goto cleanup; + rv = md_store_load_json(store, MD_SG_OCSP, ostat->md_name, ostat->file_name, &jprops, ptemp); + if (APR_SUCCESS != rv) goto cleanup; + rv = ostat_from_json(&resp_stat, &resp_der, &resp_valid, jprops, ptemp); + if (APR_SUCCESS != rv) goto cleanup; + rv = ostat_set(ostat, resp_stat, &resp_der, &resp_valid, mtime); + if (APR_SUCCESS != rv) goto cleanup; +cleanup: + return rv; +} + + +static apr_status_t ocsp_status_save(md_ocsp_cert_stat_t stat, const md_data_t *resp_der, + const md_timeperiod_t *resp_valid, + md_ocsp_status_t *ostat, apr_pool_t *ptemp) +{ + md_store_t *store = ostat->reg->store; + md_json_t *jprops; + apr_time_t mtime; + apr_status_t rv; + + jprops = md_json_create(ptemp); + ostat_to_json(jprops, stat, resp_der, resp_valid, ptemp); + rv = md_store_save_json(store, ptemp, MD_SG_OCSP, ostat->md_name, ostat->file_name, jprops, 0); + if (APR_SUCCESS != rv) goto cleanup; + mtime = md_store_get_modified(store, MD_SG_OCSP, ostat->md_name, ostat->file_name, ptemp); + if (mtime) ostat->resp_mtime = mtime; +cleanup: + return rv; +} + +static apr_status_t ocsp_reg_cleanup(void *data) +{ + md_ocsp_reg_t *reg = data; + + /* free all OpenSSL structures that we hold */ + apr_hash_do(ostat_cleanup, reg, reg->ostat_by_id); + return APR_SUCCESS; +} + +apr_status_t md_ocsp_reg_make(md_ocsp_reg_t **preg, apr_pool_t *p, md_store_t *store, + const md_timeslice_t *renew_window, + const char *user_agent, const char *proxy_url, + apr_time_t min_delay) +{ + md_ocsp_reg_t *reg; + apr_status_t rv = APR_SUCCESS; + + reg = apr_palloc(p, sizeof(*reg)); + if (!reg) { + rv = APR_ENOMEM; + goto cleanup; + } + reg->p = p; + reg->store = store; + reg->user_agent = user_agent; + reg->proxy_url = proxy_url; + reg->id_by_external_id = apr_hash_make(p); + reg->ostat_by_id = apr_hash_make(p); + reg->renew_window = *renew_window; + reg->min_delay = min_delay; + + rv = apr_thread_mutex_create(®->mutex, APR_THREAD_MUTEX_NESTED, p); + if (APR_SUCCESS != rv) goto cleanup; + + apr_pool_cleanup_register(p, reg, ocsp_reg_cleanup, apr_pool_cleanup_null); +cleanup: + *preg = (APR_SUCCESS == rv)? reg : NULL; + return rv; +} + +apr_status_t md_ocsp_prime(md_ocsp_reg_t *reg, const char *ext_id, apr_size_t ext_id_len, + md_cert_t *cert, md_cert_t *issuer, const md_t *md) +{ + md_ocsp_status_t *ostat; + const char *name; + md_data_t id; + apr_status_t rv = APR_SUCCESS; + + /* Called during post_config. no mutex protection needed */ + name = md? md->name : MD_OTHER; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, reg->p, + "md[%s]: priming OCSP status", name); + + rv = md_ocsp_init_id(&id, reg->p, cert); + if (APR_SUCCESS != rv) goto cleanup; + + ostat = apr_hash_get(reg->ostat_by_id, id.data, (apr_ssize_t)id.len); + if (ostat) goto cleanup; /* already seen it, cert is used in >1 server_rec */ + + ostat = apr_pcalloc(reg->p, sizeof(*ostat)); + ostat->id = id; + ostat->reg = reg; + ostat->md_name = name; + md_data_to_hex(&ostat->hexid, 0, reg->p, &ostat->id); + ostat->file_name = apr_psprintf(reg->p, "ocsp-%s.json", ostat->hexid); + rv = md_cert_to_sha256_fingerprint(&ostat->hex_sha256, cert, reg->p); + if (APR_SUCCESS != rv) goto cleanup; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p, + "md[%s]: getting ocsp responder from cert", name); + rv = md_cert_get_ocsp_responder_url(&ostat->responder_url, reg->p, cert); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, reg->p, + "md[%s]: certificate with serial %s has no OCSP responder URL", + name, md_cert_get_serial_number(cert, reg->p)); + goto cleanup; + } + + ostat->certid = OCSP_cert_to_id(NULL, md_cert_get_X509(cert), md_cert_get_X509(issuer)); + if (!ostat->certid) { + rv = APR_EGENERAL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, reg->p, + "md[%s]: unable to create OCSP certid for certificate with serial %s", + name, md_cert_get_serial_number(cert, reg->p)); + goto cleanup; + } + + /* See, if we have something in store */ + ocsp_status_refresh(ostat, reg->p); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, reg->p, + "md[%s]: adding ocsp info (responder=%s)", + name, ostat->responder_url); + apr_hash_set(reg->ostat_by_id, ostat->id.data, (apr_ssize_t)ostat->id.len, ostat); + if (ext_id) { + md_ocsp_id_map_t *id_map; + + id_map = apr_pcalloc(reg->p, sizeof(*id_map)); + id_map->id = id; + md_data_assign_pcopy(&id_map->external_id, ext_id, ext_id_len, reg->p); + /* check for collision/uniqness? */ + apr_hash_set(reg->id_by_external_id, id_map->external_id.data, + (apr_ssize_t)id_map->external_id.len, id_map); + } + rv = APR_SUCCESS; +cleanup: + return rv; +} + +apr_status_t md_ocsp_get_status(md_ocsp_copy_der *cb, void *userdata, md_ocsp_reg_t *reg, + const char *ext_id, apr_size_t ext_id_len, + apr_pool_t *p, const md_t *md) +{ + md_ocsp_status_t *ostat; + const char *name; + apr_status_t rv = APR_SUCCESS; + md_ocsp_id_map_t *id_map; + const char *id; + apr_size_t id_len; + int locked = 0; + + (void)p; + (void)md; + name = md? md->name : MD_OTHER; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p, + "md[%s]: OCSP, get_status", name); + + id_map = apr_hash_get(reg->id_by_external_id, ext_id, (apr_ssize_t)ext_id_len); + id = id_map? id_map->id.data : ext_id; + id_len = id_map? id_map->id.len : ext_id_len; + ostat = apr_hash_get(reg->ostat_by_id, id, (apr_ssize_t)id_len); + if (!ostat) { + rv = APR_ENOENT; + goto cleanup; + } + + /* While the ostat instance itself always exists, the response data it holds + * may vary over time and we need locked access to make a copy. */ + apr_thread_mutex_lock(reg->mutex); + locked = 1; + + if (ostat->resp_der.len <= 0) { + /* No response known, check store for new response. */ + ocsp_status_refresh(ostat, p); + if (ostat->resp_der.len <= 0) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p, + "md[%s]: OCSP, no response available", name); + cb(NULL, 0, userdata); + goto cleanup; + } + } + /* We have a response */ + if (ostat_should_renew(ostat)) { + /* But it is up for renewal. A watchdog should be busy with + * retrieving a new one. In case of outages, this might take + * a while, however. Pace the frequency of checks with the + * urgency of a new response based on the remaining time. */ + long secs = (long)apr_time_sec(md_timeperiod_remaining(&ostat->resp_valid, apr_time_now())); + apr_time_t waiting_time; + + /* every hour, every minute, every second */ + waiting_time = ((secs >= MD_SECS_PER_DAY)? + apr_time_from_sec(60 * 60) : ((secs >= 60)? + apr_time_from_sec(60) : apr_time_from_sec(1))); + if ((apr_time_now() - ostat->resp_last_check) >= waiting_time) { + ostat->resp_last_check = apr_time_now(); + ocsp_status_refresh(ostat, p); + } + } + + cb((const unsigned char*)ostat->resp_der.data, ostat->resp_der.len, userdata); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p, + "md[%s]: OCSP, provided %ld bytes of response", + name, (long)ostat->resp_der.len); +cleanup: + if (locked) apr_thread_mutex_unlock(reg->mutex); + return rv; +} + +static void ocsp_get_meta(md_ocsp_cert_stat_t *pstat, md_timeperiod_t *pvalid, + md_ocsp_reg_t *reg, md_ocsp_status_t *ostat, apr_pool_t *p) +{ + apr_thread_mutex_lock(reg->mutex); + if (ostat->resp_der.len <= 0) { + /* No response known, check the store if out watchdog retrieved one + * in the meantime. */ + ocsp_status_refresh(ostat, p); + } + *pvalid = ostat->resp_valid; + *pstat = ostat->resp_stat; + apr_thread_mutex_unlock(reg->mutex); +} + +apr_status_t md_ocsp_get_meta(md_ocsp_cert_stat_t *pstat, md_timeperiod_t *pvalid, + md_ocsp_reg_t *reg, const md_cert_t *cert, + apr_pool_t *p, const md_t *md) +{ + md_ocsp_status_t *ostat; + const char *name; + apr_status_t rv; + md_timeperiod_t valid; + md_ocsp_cert_stat_t stat; + md_data_t id; + + (void)p; + (void)md; + name = md? md->name : MD_OTHER; + memset(&valid, 0, sizeof(valid)); + stat = MD_OCSP_CERT_ST_UNKNOWN; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, reg->p, + "md[%s]: OCSP, get_status", name); + + rv = md_ocsp_init_id(&id, p, cert); + if (APR_SUCCESS != rv) goto cleanup; + + ostat = apr_hash_get(reg->ostat_by_id, id.data, (apr_ssize_t)id.len); + if (!ostat) { + rv = APR_ENOENT; + goto cleanup; + } + ocsp_get_meta(&stat, &valid, reg, ostat, p); +cleanup: + *pstat = stat; + *pvalid = valid; + return rv; +} + +apr_size_t md_ocsp_count(md_ocsp_reg_t *reg) +{ + return apr_hash_count(reg->ostat_by_id); +} + +static const char *certid_as_hex(const OCSP_CERTID *certid, apr_pool_t *p) +{ + md_data_t der; + const char *hex; + + memset(&der, 0, sizeof(der)); + der.len = (apr_size_t)i2d_OCSP_CERTID((OCSP_CERTID*)certid, (unsigned char**)&der.data); + der.free_data = md_openssl_free; + md_data_to_hex(&hex, 0, p, &der); + md_data_clear(&der); + return hex; +} + +static const char *certid_summary(const OCSP_CERTID *certid, apr_pool_t *p) +{ + const char *serial, *issuer, *key, *s; + ASN1_INTEGER *aserial; + ASN1_OCTET_STRING *aname_hash, *akey_hash; + ASN1_OBJECT *amd_nid; + BIGNUM *bn; + md_data_t data; + + serial = issuer = key = "???"; + OCSP_id_get0_info(&aname_hash, &amd_nid, &akey_hash, &aserial, (OCSP_CERTID*)certid); + if (aname_hash) { + data.len = (apr_size_t)aname_hash->length; + data.data = (const char*)aname_hash->data; + md_data_to_hex(&issuer, 0, p, &data); + } + if (akey_hash) { + data.len = (apr_size_t)akey_hash->length; + data.data = (const char*)akey_hash->data; + md_data_to_hex(&key, 0, p, &data); + } + if (aserial) { + bn = ASN1_INTEGER_to_BN(aserial, NULL); + s = BN_bn2hex(bn); + serial = apr_pstrdup(p, s); + OPENSSL_free((void*)bn); + OPENSSL_free((void*)s); + } + return apr_psprintf(p, "certid[der=%s, issuer=%s, key=%s, serial=%s]", + certid_as_hex(certid, p), issuer, key, serial); +} + +static const char *certstatus_string(int status) +{ + switch (status) { + case V_OCSP_CERTSTATUS_GOOD: return "good"; + case V_OCSP_CERTSTATUS_REVOKED: return "revoked"; + case V_OCSP_CERTSTATUS_UNKNOWN: return "unknown"; + default: return "???"; + } + +} + +static const char *single_resp_summary(OCSP_SINGLERESP* resp, apr_pool_t *p) +{ + const OCSP_CERTID *certid; + int status, reason = 0; + ASN1_GENERALIZEDTIME *bup = NULL, *bnextup = NULL; + md_timeperiod_t valid; + +#if MD_USE_OPENSSL_PRE_1_1_API + certid = resp->certId; +#else + certid = OCSP_SINGLERESP_get0_id(resp); +#endif + status = OCSP_single_get0_status(resp, &reason, NULL, &bup, &bnextup); + valid.start = bup? md_asn1_generalized_time_get(bup) : apr_time_now(); + valid.end = md_asn1_generalized_time_get(bnextup); + + return apr_psprintf(p, "ocsp-single-resp[%s, status=%s, reason=%d, valid=%s]", + certid_summary(certid, p), + certstatus_string(status), reason, + md_timeperiod_print(p, &valid)); +} + +typedef struct { + apr_pool_t *p; + md_ocsp_status_t *ostat; + md_result_t *result; + md_job_t *job; +} md_ocsp_update_t; + +static apr_status_t ostat_on_resp(const md_http_response_t *resp, void *baton) +{ + md_ocsp_update_t *update = baton; + md_ocsp_status_t *ostat = update->ostat; + md_http_request_t *req = resp->req; + OCSP_RESPONSE *ocsp_resp = NULL; + OCSP_BASICRESP *basic_resp = NULL; + OCSP_SINGLERESP *single_resp; + apr_status_t rv = APR_SUCCESS; + int n, breason = 0, bstatus; + ASN1_GENERALIZEDTIME *bup = NULL, *bnextup = NULL; + md_data_t der, new_der; + md_timeperiod_t valid; + md_ocsp_cert_stat_t nstat; + + der.data = new_der.data = NULL; + der.len = new_der.len = 0; + + md_result_activity_printf(update->result, "status of certid %s, reading response", + ostat->hexid); + if (APR_SUCCESS != (rv = apr_brigade_pflatten(resp->body, (char**)&der.data, + &der.len, req->pool))) { + goto cleanup; + } + if (NULL == (ocsp_resp = d2i_OCSP_RESPONSE(NULL, (const unsigned char**)&der.data, + (long)der.len))) { + rv = APR_EINVAL; + + md_result_set(update->result, rv, + apr_psprintf(req->pool, "req[%d] response body does not parse as " + "OCSP response, status=%d, body brigade length=%ld", + resp->req->id, resp->status, (long)der.len)); + md_result_log(update->result, MD_LOG_DEBUG); + goto cleanup; + } + /* got a response! but what does it say? */ + n = OCSP_response_status(ocsp_resp); + if (OCSP_RESPONSE_STATUS_SUCCESSFUL != n) { + rv = APR_EINVAL; + md_result_printf(update->result, rv, "OCSP response status is, unsuccessfully, %d", n); + md_result_log(update->result, MD_LOG_DEBUG); + goto cleanup; + } + basic_resp = OCSP_response_get1_basic(ocsp_resp); + if (!basic_resp) { + rv = APR_EINVAL; + md_result_set(update->result, rv, "OCSP response has no basicresponse"); + md_result_log(update->result, MD_LOG_DEBUG); + goto cleanup; + } + /* The notion of nonce enabled freshness in OCSP responses, e.g. that the response + * contains the signed nonce we sent to the responder, does not scale well. Responders + * like to return cached response bytes and therefore do not add a nonce to it. + * So, in reality, we can only detect a mismatch when present and otherwise have + * to accept it. */ + switch ((n = OCSP_check_nonce(ostat->ocsp_req, basic_resp))) { + case 1: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, + "req[%d]: OCSP response nonce does match", req->id); + break; + case 0: + rv = APR_EINVAL; + md_result_printf(update->result, rv, "OCSP nonce mismatch in response", n); + md_result_log(update->result, MD_LOG_WARNING); + goto cleanup; + + case -1: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, + "req[%d]: OCSP response did not return the nonce", req->id); + break; + default: + break; + } + + if (!OCSP_resp_find_status(basic_resp, ostat->certid, &bstatus, + &breason, NULL, &bup, &bnextup)) { + const char *prefix, *slist = "", *sep = ""; + int i; + + rv = APR_EINVAL; + prefix = apr_psprintf(req->pool, "OCSP response, no matching status reported for %s", + certid_summary(ostat->certid, req->pool)); + for (i = 0; i < OCSP_resp_count(basic_resp); ++i) { + single_resp = OCSP_resp_get0(basic_resp, i); + slist = apr_psprintf(req->pool, "%s%s%s", slist, sep, + single_resp_summary(single_resp, req->pool)); + sep = ", "; + } + md_result_printf(update->result, rv, "%s, status list [%s]", prefix, slist); + md_result_log(update->result, MD_LOG_DEBUG); + goto cleanup; + } + if (V_OCSP_CERTSTATUS_UNKNOWN == bstatus) { + rv = APR_ENOENT; + md_result_set(update->result, rv, "OCSP basicresponse says cert is unknown"); + md_result_log(update->result, MD_LOG_DEBUG); + goto cleanup; + } + if (!bnextup) { + rv = APR_EINVAL; + md_result_set(update->result, rv, "OCSP basicresponse reports not valid dates"); + md_result_log(update->result, MD_LOG_DEBUG); + goto cleanup; + } + + /* Coming here, we have a response for our certid and it is either GOOD + * or REVOKED. Both cases we want to remember and use in stapling. */ + n = i2d_OCSP_RESPONSE(ocsp_resp, (unsigned char**)&new_der.data); + if (n <= 0) { + rv = APR_EGENERAL; + md_result_set(update->result, rv, "error DER encoding OCSP response"); + md_result_log(update->result, MD_LOG_WARNING); + goto cleanup; + } + new_der.len = (apr_size_t)n; + new_der.free_data = md_openssl_free; + nstat = (bstatus == V_OCSP_CERTSTATUS_GOOD)? MD_OCSP_CERT_ST_GOOD : MD_OCSP_CERT_ST_REVOKED; + valid.start = bup? md_asn1_generalized_time_get(bup) : apr_time_now(); + valid.end = md_asn1_generalized_time_get(bnextup); + + /* First, update the instance with a copy */ + apr_thread_mutex_lock(ostat->reg->mutex); + ostat_set(ostat, nstat, &new_der, &valid, apr_time_now()); + apr_thread_mutex_unlock(ostat->reg->mutex); + + /* Next, save the original response */ + rv = ocsp_status_save(nstat, &new_der, &valid, ostat, req->pool); + if (APR_SUCCESS != rv) { + md_result_set(update->result, rv, "error saving OCSP status"); + md_result_log(update->result, MD_LOG_ERR); + goto cleanup; + } + + md_result_printf(update->result, rv, "certificate status is %s, status valid %s", + (nstat == MD_OCSP_CERT_ST_GOOD)? "GOOD" : "REVOKED", + md_timeperiod_print(req->pool, &ostat->resp_valid)); + md_result_log(update->result, MD_LOG_DEBUG); + +cleanup: + md_data_clear(&new_der); + if (basic_resp) OCSP_BASICRESP_free(basic_resp); + if (ocsp_resp) OCSP_RESPONSE_free(ocsp_resp); + return rv; +} + +static apr_status_t ostat_on_req_status(const md_http_request_t *req, apr_status_t status, + void *baton) +{ + md_ocsp_update_t *update = baton; + md_ocsp_status_t *ostat = update->ostat; + + (void)req; + md_job_end_run(update->job, update->result); + if (APR_SUCCESS != status) { + ++ostat->errors; + ostat->next_run = apr_time_now() + md_job_delay_on_errors(update->job, ostat->errors, NULL); + md_result_printf(update->result, status, "OCSP status update failed (%d. time)", + ostat->errors); + md_result_log(update->result, MD_LOG_DEBUG); + md_job_log_append(update->job, "ocsp-error", + update->result->problem, update->result->detail); + md_event_holler("ocsp-errored", update->job->mdomain, update->job, update->result, update->p); + goto cleanup; + } + md_event_holler("ocsp-renewed", update->job->mdomain, update->job, update->result, update->p); + +cleanup: + md_job_save(update->job, update->result, update->p); + ostat_req_cleanup(ostat); + return APR_SUCCESS; +} + +typedef struct { + md_ocsp_reg_t *reg; + apr_array_header_t *todos; + apr_pool_t *ptemp; + apr_time_t time; + int max_parallel; +} md_ocsp_todo_ctx_t; + +static apr_status_t ocsp_req_make(OCSP_REQUEST **pocsp_req, OCSP_CERTID *certid) +{ + OCSP_REQUEST *req = NULL; + OCSP_CERTID *id_copy = NULL; + apr_status_t rv = APR_ENOMEM; + + req = OCSP_REQUEST_new(); + if (!req) goto cleanup; + id_copy = OCSP_CERTID_dup(certid); + if (!id_copy) goto cleanup; + if (!OCSP_request_add0_id(req, id_copy)) goto cleanup; + id_copy = NULL; + OCSP_request_add1_nonce(req, 0, -1); + rv = APR_SUCCESS; +cleanup: + if (id_copy) OCSP_CERTID_free(id_copy); + if (APR_SUCCESS != rv && req) { + OCSP_REQUEST_free(req); + req = NULL; + } + *pocsp_req = req; + return rv; +} + +static apr_status_t ocsp_req_assign_der(md_data_t *d, OCSP_REQUEST *ocsp_req) +{ + int len; + + md_data_clear(d); + len = i2d_OCSP_REQUEST(ocsp_req, (unsigned char**)&d->data); + if (len < 0) return APR_ENOMEM; + d->len = (apr_size_t)len; + d->free_data = md_openssl_free; + return APR_SUCCESS; +} + +static apr_status_t next_todo(md_http_request_t **preq, void *baton, + md_http_t *http, int in_flight) +{ + md_ocsp_todo_ctx_t *ctx = baton; + md_ocsp_update_t *update, **pupdate; + md_ocsp_status_t *ostat; + md_http_request_t *req = NULL; + apr_status_t rv = APR_ENOENT; + apr_table_t *headers; + + if (in_flight < ctx->max_parallel) { + pupdate = apr_array_pop(ctx->todos); + if (pupdate) { + update = *pupdate; + ostat = update->ostat; + + update->job = md_ocsp_job_make(ctx->reg, ostat->md_name, update->p); + md_job_load(update->job); + md_job_start_run(update->job, update->result, ctx->reg->store); + + if (!ostat->ocsp_req) { + rv = ocsp_req_make(&ostat->ocsp_req, ostat->certid); + if (APR_SUCCESS != rv) goto cleanup; + } + if (0 == ostat->req_der.len) { + rv = ocsp_req_assign_der(&ostat->req_der, ostat->ocsp_req); + if (APR_SUCCESS != rv) goto cleanup; + } + md_result_activity_printf(update->result, "status of certid %s, " + "contacting %s", ostat->hexid, ostat->responder_url); + headers = apr_table_make(ctx->ptemp, 5); + apr_table_set(headers, "Expect", ""); + rv = md_http_POSTd_create(&req, http, ostat->responder_url, headers, + "application/ocsp-request", &ostat->req_der); + if (APR_SUCCESS != rv) goto cleanup; + md_http_set_on_status_cb(req, ostat_on_req_status, update); + md_http_set_on_response_cb(req, ostat_on_resp, update); + rv = APR_SUCCESS; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, req->pool, + "scheduling OCSP request[%d] for %s, %d request in flight", + req->id, ostat->md_name, in_flight); + } + } +cleanup: + *preq = (APR_SUCCESS == rv)? req : NULL; + return rv; +} + +static int select_updates(void *baton, const void *key, apr_ssize_t klen, const void *val) +{ + md_ocsp_todo_ctx_t *ctx = baton; + md_ocsp_status_t *ostat = (md_ocsp_status_t *)val; + md_ocsp_update_t *update; + + (void)key; + (void)klen; + if (ostat->next_run <= ctx->time) { + update = apr_pcalloc(ctx->ptemp, sizeof(*update)); + update->p = ctx->ptemp; + update->ostat = ostat; + update->result = md_result_md_make(update->p, ostat->md_name); + update->job = NULL; + APR_ARRAY_PUSH(ctx->todos, md_ocsp_update_t*) = update; + } + return 1; +} + +static int select_next_run(void *baton, const void *key, apr_ssize_t klen, const void *val) +{ + md_ocsp_todo_ctx_t *ctx = baton; + md_ocsp_status_t *ostat = (md_ocsp_status_t *)val; + + (void)key; + (void)klen; + if (ostat->next_run < ctx->time && ostat->next_run > apr_time_now()) { + ctx->time = ostat->next_run; + } + return 1; +} + +void md_ocsp_renew(md_ocsp_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, apr_time_t *pnext_run) +{ + md_ocsp_todo_ctx_t ctx; + md_http_t *http; + apr_status_t rv = APR_SUCCESS; + + (void)p; + (void)pnext_run; + + ctx.reg = reg; + ctx.ptemp = ptemp; + ctx.todos = apr_array_make(ptemp, (int)md_ocsp_count(reg), sizeof(md_ocsp_status_t*)); + ctx.max_parallel = 6; /* the magic number in HTTP */ + + /* Create a list of update tasks that are needed now or in the next minute */ + ctx.time = apr_time_now() + apr_time_from_sec(60);; + apr_hash_do(select_updates, &ctx, reg->ostat_by_id); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "OCSP status updates due: %d", ctx.todos->nelts); + if (!ctx.todos->nelts) goto cleanup; + + rv = md_http_create(&http, ptemp, reg->user_agent, reg->proxy_url); + if (APR_SUCCESS != rv) goto cleanup; + + rv = md_http_multi_perform(http, next_todo, &ctx); + +cleanup: + /* When do we need to run next? *pnext_run contains the planned schedule from + * the watchdog. We can make that earlier if we need it. */ + ctx.time = *pnext_run; + apr_hash_do(select_next_run, &ctx, reg->ostat_by_id); + + /* sanity check and return */ + if (ctx.time < apr_time_now()) ctx.time = apr_time_now() + apr_time_from_sec(1); + *pnext_run = ctx.time; + + if (APR_SUCCESS != rv && APR_ENOENT != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "ocsp_renew done"); + } + return; +} + +apr_status_t md_ocsp_remove_responses_older_than(md_ocsp_reg_t *reg, apr_pool_t *p, + apr_time_t timestamp) +{ + return md_store_remove_not_modified_since(reg->store, p, timestamp, + MD_SG_OCSP, "*", "ocsp*.json"); +} + +typedef struct { + apr_pool_t *p; + md_ocsp_reg_t *reg; + int good; + int revoked; + int unknown; +} ocsp_summary_ctx_t; + +static int add_to_summary(void *baton, const void *key, apr_ssize_t klen, const void *val) +{ + ocsp_summary_ctx_t *ctx = baton; + md_ocsp_status_t *ostat = (md_ocsp_status_t *)val; + md_ocsp_cert_stat_t stat; + md_timeperiod_t valid; + + (void)key; + (void)klen; + ocsp_get_meta(&stat, &valid, ctx->reg, ostat, ctx->p); + switch (stat) { + case MD_OCSP_CERT_ST_GOOD: ++ctx->good; break; + case MD_OCSP_CERT_ST_REVOKED: ++ctx->revoked; break; + case MD_OCSP_CERT_ST_UNKNOWN: ++ctx->unknown; break; + } + return 1; +} + +void md_ocsp_get_summary(md_json_t **pjson, md_ocsp_reg_t *reg, apr_pool_t *p) +{ + md_json_t *json; + ocsp_summary_ctx_t ctx; + + memset(&ctx, 0, sizeof(ctx)); + ctx.p = p; + ctx.reg = reg; + apr_hash_do(add_to_summary, &ctx, reg->ostat_by_id); + + json = md_json_create(p); + md_json_setl(ctx.good+ctx.revoked+ctx.unknown, json, MD_KEY_TOTAL, NULL); + md_json_setl(ctx.good, json, MD_KEY_GOOD, NULL); + md_json_setl(ctx.revoked, json, MD_KEY_REVOKED, NULL); + md_json_setl(ctx.unknown, json, MD_KEY_UNKNOWN, NULL); + *pjson = json; +} + +static apr_status_t job_loadj(md_json_t **pjson, const char *name, + md_ocsp_reg_t *reg, apr_pool_t *p) +{ + return md_store_load_json(reg->store, MD_SG_OCSP, name, MD_FN_JOB, pjson, p); +} + +typedef struct { + apr_pool_t *p; + md_ocsp_reg_t *reg; + apr_array_header_t *ostats; +} ocsp_status_ctx_t; + +static md_json_t *mk_jstat(md_ocsp_status_t *ostat, md_ocsp_reg_t *reg, apr_pool_t *p) +{ + md_ocsp_cert_stat_t stat; + md_timeperiod_t valid, renewal; + md_json_t *json, *jobj; + apr_status_t rv; + + json = md_json_create(p); + md_json_sets(ostat->md_name, json, MD_KEY_DOMAIN, NULL); + md_json_sets(ostat->hexid, json, MD_KEY_ID, NULL); + ocsp_get_meta(&stat, &valid, reg, ostat, p); + md_json_sets(md_ocsp_cert_stat_name(stat), json, MD_KEY_STATUS, NULL); + md_json_sets(ostat->hex_sha256, json, MD_KEY_CERT, MD_KEY_SHA256_FINGERPRINT, NULL); + md_json_sets(ostat->responder_url, json, MD_KEY_URL, NULL); + md_json_set_timeperiod(&valid, json, MD_KEY_VALID, NULL); + renewal = md_timeperiod_slice_before_end(&valid, ®->renew_window); + md_json_set_time(renewal.start, json, MD_KEY_RENEW_AT, NULL); + if ((MD_OCSP_CERT_ST_UNKNOWN == stat) || renewal.start < apr_time_now()) { + /* We have no answer yet, or it should be in renew now. Add job information */ + rv = job_loadj(&jobj, ostat->md_name, reg, p); + if (APR_SUCCESS == rv) { + md_json_setj(jobj, json, MD_KEY_RENEWAL, NULL); + } + } + return json; +} + +static int add_ostat(void *baton, const void *key, apr_ssize_t klen, const void *val) +{ + ocsp_status_ctx_t *ctx = baton; + const md_ocsp_status_t *ostat = val; + + (void)key; + (void)klen; + APR_ARRAY_PUSH(ctx->ostats, const md_ocsp_status_t*) = ostat; + return 1; +} + +static int md_ostat_cmp(const void *v1, const void *v2) +{ + int n; + n = strcmp((*(md_ocsp_status_t**)v1)->md_name, (*(md_ocsp_status_t**)v2)->md_name); + if (!n) { + n = strcmp((*(md_ocsp_status_t**)v1)->hexid, (*(md_ocsp_status_t**)v2)->hexid); + } + return n; +} + +void md_ocsp_get_status_all(md_json_t **pjson, md_ocsp_reg_t *reg, apr_pool_t *p) +{ + md_json_t *json; + ocsp_status_ctx_t ctx; + md_ocsp_status_t *ostat; + int i; + + memset(&ctx, 0, sizeof(ctx)); + ctx.p = p; + ctx.reg = reg; + ctx.ostats = apr_array_make(p, (int)apr_hash_count(reg->ostat_by_id), sizeof(md_ocsp_status_t*)); + json = md_json_create(p); + + apr_hash_do(add_ostat, &ctx, reg->ostat_by_id); + qsort(ctx.ostats->elts, (size_t)ctx.ostats->nelts, sizeof(md_json_t*), md_ostat_cmp); + + for (i = 0; i < ctx.ostats->nelts; ++i) { + ostat = APR_ARRAY_IDX(ctx.ostats, i, md_ocsp_status_t*); + md_json_addj(mk_jstat(ostat, reg, p), json, MD_KEY_OCSPS, NULL); + } + *pjson = json; +} + +md_job_t *md_ocsp_job_make(md_ocsp_reg_t *ocsp, const char *mdomain, apr_pool_t *p) +{ + return md_job_make(p, ocsp->store, MD_SG_OCSP, mdomain, ocsp->min_delay); +} diff --git a/modules/md/md_ocsp.h b/modules/md/md_ocsp.h new file mode 100644 index 0000000..c91dc54 --- /dev/null +++ b/modules/md/md_ocsp.h @@ -0,0 +1,71 @@ +/* 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_ocsp_h +#define md_ocsp_h + +struct md_data_t; +struct md_job_t; +struct md_json_t; +struct md_result_t; +struct md_store_t; +struct md_timeslice_t; + +typedef enum { + MD_OCSP_CERT_ST_UNKNOWN, + MD_OCSP_CERT_ST_GOOD, + MD_OCSP_CERT_ST_REVOKED, +} md_ocsp_cert_stat_t; + +const char *md_ocsp_cert_stat_name(md_ocsp_cert_stat_t stat); +md_ocsp_cert_stat_t md_ocsp_cert_stat_value(const char *name); + +typedef struct md_ocsp_reg_t md_ocsp_reg_t; + +apr_status_t md_ocsp_reg_make(md_ocsp_reg_t **preg, apr_pool_t *p, + struct md_store_t *store, + const md_timeslice_t *renew_window, + const char *user_agent, const char *proxy_url, + apr_time_t min_delay); + +apr_status_t md_ocsp_init_id(struct md_data_t *id, apr_pool_t *p, const md_cert_t *cert); + +apr_status_t md_ocsp_prime(md_ocsp_reg_t *reg, const char *ext_id, apr_size_t ext_id_len, + md_cert_t *x, md_cert_t *issuer, const md_t *md); + +typedef void md_ocsp_copy_der(const unsigned char *der, apr_size_t der_len, void *userdata); + +apr_status_t md_ocsp_get_status(md_ocsp_copy_der *cb, void *userdata, md_ocsp_reg_t *reg, + const char *ext_id, apr_size_t ext_id_len, + apr_pool_t *p, const md_t *md); + +apr_status_t md_ocsp_get_meta(md_ocsp_cert_stat_t *pstat, md_timeperiod_t *pvalid, + md_ocsp_reg_t *reg, const md_cert_t *cert, + apr_pool_t *p, const md_t *md); + +apr_size_t md_ocsp_count(md_ocsp_reg_t *reg); + +void md_ocsp_renew(md_ocsp_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, apr_time_t *pnext_run); + +apr_status_t md_ocsp_remove_responses_older_than(md_ocsp_reg_t *reg, apr_pool_t *p, + apr_time_t timestamp); + +void md_ocsp_get_summary(struct md_json_t **pjson, md_ocsp_reg_t *reg, apr_pool_t *p); +void md_ocsp_get_status_all(struct md_json_t **pjson, md_ocsp_reg_t *reg, apr_pool_t *p); + +struct md_job_t *md_ocsp_job_make(md_ocsp_reg_t *ocsp, const char *mdomain, apr_pool_t *p); + +#endif /* md_ocsp_h */ diff --git a/modules/md/md_reg.c b/modules/md/md_reg.c index 233fea7..8bceb0e 100644 --- a/modules/md/md_reg.c +++ b/modules/md/md_reg.c @@ -26,21 +26,37 @@ #include "md.h" #include "md_crypt.h" +#include "md_event.h" #include "md_log.h" #include "md_json.h" +#include "md_result.h" #include "md_reg.h" #include "md_store.h" +#include "md_status.h" +#include "md_tailscale.h" #include "md_util.h" #include "md_acme.h" #include "md_acme_acct.h" struct md_reg_t { + apr_pool_t *p; struct md_store_t *store; struct apr_hash_t *protos; + struct apr_hash_t *certs; int can_http; int can_https; const char *proxy_url; + const char *ca_file; + int domains_frozen; + md_timeslice_t *renew_window; + md_timeslice_t *warn_window; + md_job_notify_cb *notify; + void *notify_ctx; + apr_time_t min_delay; + int retry_failover; + int use_store_locks; + apr_time_t lock_wait_timeout; }; /**************************************************************************************************/ @@ -67,20 +83,34 @@ static apr_status_t load_props(md_reg_t *reg, apr_pool_t *p) 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) +apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *p, struct md_store_t *store, + const char *proxy_url, const char *ca_file, + apr_time_t min_delay, int retry_failover, + int use_store_locks, apr_time_t lock_wait_timeout) { md_reg_t *reg; apr_status_t rv; reg = apr_pcalloc(p, sizeof(*reg)); + reg->p = p; reg->store = store; reg->protos = apr_hash_make(p); + reg->certs = apr_hash_make(p); reg->can_http = 1; reg->can_https = 1; reg->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL; + reg->ca_file = (ca_file && apr_strnatcasecmp("none", ca_file))? + apr_pstrdup(p, ca_file) : NULL; + reg->min_delay = min_delay; + reg->retry_failover = retry_failover; + reg->use_store_locks = use_store_locks; + reg->lock_wait_timeout = lock_wait_timeout; + + md_timeslice_create(®->renew_window, p, MD_TIME_LIFE_NORM, MD_TIME_RENEW_WINDOW_DEF); + md_timeslice_create(®->warn_window, p, MD_TIME_LIFE_NORM, MD_TIME_WARN_WINDOW_DEF); - if (APR_SUCCESS == (rv = md_acme_protos_add(reg->protos, p))) { + if (APR_SUCCESS == (rv = md_acme_protos_add(reg->protos, p)) + && APR_SUCCESS == (rv = md_tailscale_protos_add(reg->protos, p))) { rv = load_props(reg, p); } @@ -114,7 +144,7 @@ static apr_status_t check_values(md_reg_t *reg, apr_pool_t *p, const md_t *md, i 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)) { + if (!md_dns_is_name(p, domain, 1) && !md_dns_is_wildcard(p, domain)) { 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; @@ -145,12 +175,17 @@ static apr_status_t check_values(md_reg_t *reg, apr_pool_t *p, const md_t *md, i } } - 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_URL & fields) && md->ca_urls) { /* setting to empty is ok */ + int i; + const char *url; + for (i = 0; i < md->ca_urls->nelts; ++i) { + url = APR_ARRAY_IDX(md->ca_urls, i, const char*); + rv = md_util_abs_uri_check(p, 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, url); + return APR_EINVAL; + } } } @@ -162,7 +197,8 @@ static apr_status_t check_values(md_reg_t *reg, apr_pool_t *p, const md_t *md, i /* 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 */ + if ((MD_UPD_AGREEMENT & fields) && md->ca_agreement + && strcmp("accepted", 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, @@ -177,138 +213,72 @@ static apr_status_t check_values(md_reg_t *reg, apr_pool_t *p, const md_t *md, i /**************************************************************************************************/ /* state assessment */ -static apr_status_t state_init(md_reg_t *reg, apr_pool_t *p, md_t *md, int save_changes) +static apr_status_t state_init(md_reg_t *reg, apr_pool_t *p, md_t *md) { - md_state_t state = MD_S_UNKNOWN; - const md_creds_t *creds; + md_state_t state = MD_S_COMPLETE; + const char *state_descr = NULL; + const md_pubcert_t *pub; const md_cert_t *cert; - apr_time_t expires = 0, valid_from = 0; - apr_status_t rv; + const md_pkey_spec_t *spec; + apr_status_t rv = APR_SUCCESS; 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)) { + if (md->renew_window == NULL) md->renew_window = reg->renew_window; + if (md->warn_window == NULL) md->warn_window = reg->warn_window; + + if (md->domains && md->domains->pool != p) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "md{%s}: state_init called with foreign pool", md->name); + } + + for (i = 0; i < md_cert_count(md); ++i) { + spec = md_pkeys_spec_get(md->pks, i); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p, + "md{%s}: check cert %s", md->name, md_pkey_spec_name(spec)); + rv = md_reg_get_pubcert(&pub, reg, md, i, p); + if (APR_SUCCESS == rv) { + cert = APR_ARRAY_IDX(pub->certs, 0, const md_cert_t*); + if (!md_is_covered_by_alt_names(md, pub->alt_names)) { 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; + state_descr = apr_psprintf(p, "certificate(%s) does not cover all domains.", + md_pkey_spec_name(spec)); + goto cleanup; } - if (!md->must_staple != !md_cert_must_staple(creds->cert)) { + if (!md->must_staple != !md_cert_must_staple(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", + state_descr = apr_psprintf(p, "'must-staple' is%s requested, but " + "certificate(%s) has it%s enabled.", + md->must_staple? "" : " not", + md_pkey_spec_name(spec), !md->must_staple? "" : " not"); - goto out; + goto cleanup; } - - 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); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "md{%s}: certificate(%d) is ok", + md->name, i); + } + else if (APR_STATUS_IS_ENOENT(rv)) { + state = MD_S_INCOMPLETE; + state_descr = apr_psprintf(p, "certificate(%s) is missing", + md_pkey_spec_name(spec)); + rv = APR_SUCCESS; + goto cleanup; + } + else { + state = MD_S_ERROR; + state_descr = "error initializing"; + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "md{%s}: error", md->name); + goto cleanup; } } -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; - } +cleanup: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p, "md{%s}: state=%d, %s", + md->name, state, state_descr); 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); - } + md->state_descr = state_descr; 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 */ @@ -326,7 +296,7 @@ static int reg_md_iter(void *baton, md_store_t *store, md_t *md, apr_pool_t *pte (void)store; if (!ctx->exclude || strcmp(ctx->exclude, md->name)) { - state_init(ctx->reg, ptemp, (md_t*)md, 1); + state_init(ctx->reg, ptemp, (md_t*)md); return ctx->cb(ctx->baton, ctx->reg, md); } return 1; @@ -357,7 +327,7 @@ 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); + state_init(reg, p, md); return md; } return NULL; @@ -389,7 +359,7 @@ md_t *md_reg_find(md_reg_t *reg, const char *domain, apr_pool_t *p) md_reg_do(find_domain, &ctx, reg, p); if (ctx.md) { - state_init(reg, p, ctx.md, 1); + state_init(reg, p, ctx.md); } return ctx.md; } @@ -427,23 +397,11 @@ md_t *md_reg_find_overlap(md_reg_t *reg, const md_t *md, const char **pdomain, a *pdomain = ctx.s; } if (ctx.md) { - state_init(reg, p, ctx.md, 1); + state_init(reg, p, ctx.md); } 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 */ @@ -452,19 +410,28 @@ static apr_status_t p_md_add(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l md_reg_t *reg = baton; apr_status_t rv = APR_SUCCESS; md_t *md, *mine; + int do_check; md = va_arg(ap, md_t *); + do_check = va_arg(ap, int); + + if (reg->domains_frozen) return APR_EACCES; 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))) { - } + if (do_check && APR_SUCCESS != (rv = check_values(reg, ptemp, md, MD_UPD_ALL))) goto leave; + if (APR_SUCCESS != (rv = state_init(reg, ptemp, mine))) goto leave; + rv = md_save(reg->store, p, MD_SG_DOMAINS, mine, 1); +leave: return rv; } +static apr_status_t add_md(md_reg_t *reg, md_t *md, apr_pool_t *p, int do_checks) +{ + return md_util_pool_vdo(p_md_add, reg, p, md, do_checks, NULL); +} + 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); + return add_md(reg, md, p, 1); } static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) @@ -473,31 +440,34 @@ static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, v apr_status_t rv = APR_SUCCESS; const char *name; const md_t *md, *updates; - int fields; + int fields, do_checks; md_t *nmd; name = va_arg(ap, const char *); updates = va_arg(ap, const md_t *); fields = va_arg(ap, int); + do_checks = 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); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "md[%s]: update store", name); - if (APR_SUCCESS != (rv = check_values(reg, ptemp, updates, fields))) { + if (do_checks && APR_SUCCESS != (rv = check_values(reg, ptemp, updates, fields))) { return rv; } + if (reg->domains_frozen) return APR_EACCES; 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; + nmd->ca_urls = (updates->ca_urls? + apr_array_copy(p, updates->ca_urls) : NULL); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca url: %s", name); } if (MD_UPD_CA_PROTO & fields) { @@ -516,18 +486,17 @@ static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, v 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; + nmd->renew_mode = updates->renew_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; + *nmd->renew_window = *updates->renew_window; + } + if (MD_UPD_WARN_WINDOW & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update warn-window: %s", name); + *nmd->warn_window = *updates->warn_window; } if (MD_UPD_CA_CHALLENGES & fields) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca challenges: %s", name); @@ -536,10 +505,7 @@ static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, v } 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)); - } + nmd->pks = md_pkeys_spec_clone(p, updates->pks); } if (MD_UPD_REQUIRE_HTTPS & fields) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update require-https: %s", name); @@ -553,118 +519,239 @@ static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, v md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update must-staple: %s", name); nmd->must_staple = updates->must_staple; } + if (MD_UPD_PROTO & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update proto: %s", name); + nmd->acme_tls_1_domains = updates->acme_tls_1_domains; + } + if (MD_UPD_STAPLING & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update stapling: %s", name); + nmd->stapling = updates->stapling; + } if (fields && APR_SUCCESS == (rv = md_save(reg->store, p, MD_SG_DOMAINS, nmd, 0))) { - rv = state_init(reg, ptemp, nmd, 0); + rv = state_init(reg, ptemp, nmd); } 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) + const char *name, const md_t *md, int fields, + int do_checks) { - return md_util_pool_vdo(p_md_update, reg, p, name, md, fields, NULL); + return md_util_pool_vdo(p_md_update, reg, p, name, md, fields, do_checks, NULL); } -/**************************************************************************************************/ -/* certificate related */ - -static int ok_or_noent(apr_status_t rv) +apr_status_t md_reg_delete_acct(md_reg_t *reg, apr_pool_t *p, const char *acct_id) { - return (APR_SUCCESS == rv || APR_ENOENT == rv); + apr_status_t rv = APR_SUCCESS; + + rv = md_store_remove(reg->store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCOUNT, p, 1); + if (APR_SUCCESS == rv) { + md_store_remove(reg->store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCT_KEY, p, 1); + } + return rv; } -static apr_status_t creds_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +/**************************************************************************************************/ +/* certificate related */ + +static apr_status_t pubcert_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; + apr_array_header_t *certs; + md_pubcert_t *pubcert, **ppubcert; const md_t *md; + int index; + const md_cert_t *cert; md_cert_state_t cert_state; md_store_group_t group; apr_status_t rv; - pcreds = va_arg(ap, md_creds_t **); + ppubcert = va_arg(ap, md_pubcert_t **); group = (md_store_group_t)va_arg(ap, int); md = va_arg(ap, const md_t *); + index = va_arg(ap, int); - 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; - } - } + if (md->cert_files && md->cert_files->nelts) { + rv = md_chain_fload(&certs, p, APR_ARRAY_IDX(md->cert_files, index, const char *)); + } + else { + md_pkey_spec_t *spec = md_pkeys_spec_get(md->pks, index);; + rv = md_pubcert_load(reg->store, group, md->name, spec, &certs, p); + } + if (APR_SUCCESS != rv) goto leave; + if (certs->nelts == 0) { + rv = APR_ENOENT; + goto leave; + } + + pubcert = apr_pcalloc(p, sizeof(*pubcert)); + pubcert->certs = certs; + cert = APR_ARRAY_IDX(certs, 0, const md_cert_t *); + if (APR_SUCCESS != (rv = md_cert_get_alt_names(&pubcert->alt_names, cert, p))) goto leave; + switch ((cert_state = md_cert_state_get(cert))) { + case MD_CERT_VALID: + case MD_CERT_EXPIRED: + 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; +leave: + *ppubcert = (APR_SUCCESS == rv)? pubcert : 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 md_reg_get_pubcert(const md_pubcert_t **ppubcert, md_reg_t *reg, + const md_t *md, int i, 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; + const md_pubcert_t *pubcert; + const char *name; + + name = apr_psprintf(p, "%s[%d]", md->name, i); + pubcert = apr_hash_get(reg->certs, name, (apr_ssize_t)strlen(name)); + if (!pubcert && !reg->domains_frozen) { + rv = md_util_pool_vdo(pubcert_load, reg, reg->p, &pubcert, MD_SG_DOMAINS, md, i, NULL); + if (APR_STATUS_IS_ENOENT(rv)) { + /* We cache it missing with an empty record */ + pubcert = apr_pcalloc(reg->p, sizeof(*pubcert)); + } + else if (APR_SUCCESS != rv) goto leave; + if (p != reg->p) name = apr_pstrdup(reg->p, name); + apr_hash_set(reg->certs, name, (apr_ssize_t)strlen(name), pubcert); + } +leave: + if (APR_SUCCESS == rv && (!pubcert || !pubcert->certs)) { + rv = APR_ENOENT; + } + *ppubcert = (APR_SUCCESS == rv)? pubcert : NULL; return rv; } -/**************************************************************************************************/ -/* synching */ +apr_status_t md_reg_get_cred_files(const char **pkeyfile, const char **pcertfile, + md_reg_t *reg, md_store_group_t group, + const md_t *md, md_pkey_spec_t *spec, apr_pool_t *p) +{ + apr_status_t rv; + + rv = md_store_get_fname(pkeyfile, reg->store, group, md->name, md_pkey_filename(spec, p), p); + if (APR_SUCCESS != rv) return rv; + if (!md_file_exists(*pkeyfile, p)) return APR_ENOENT; + rv = md_store_get_fname(pcertfile, reg->store, group, md->name, md_chain_filename(spec, p), p); + if (APR_SUCCESS != rv) return rv; + if (!md_file_exists(*pcertfile, p)) return APR_ENOENT; + return APR_SUCCESS; +} -typedef struct { - apr_pool_t *p; - apr_array_header_t *store_mds; -} sync_ctx; +apr_time_t md_reg_valid_until(md_reg_t *reg, const md_t *md, apr_pool_t *p) +{ + const md_pubcert_t *pub; + const md_cert_t *cert; + int i; + apr_time_t t, valid_until = 0; + apr_status_t rv; + + for (i = 0; i < md_cert_count(md); ++i) { + rv = md_reg_get_pubcert(&pub, reg, md, i, p); + if (APR_SUCCESS == rv) { + cert = APR_ARRAY_IDX(pub->certs, 0, const md_cert_t*); + t = md_cert_get_not_after(cert); + if (valid_until == 0 || t < valid_until) { + valid_until = t; + } + } + } + return valid_until; +} -static int do_add_md(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp) +apr_time_t md_reg_renew_at(md_reg_t *reg, const md_t *md, apr_pool_t *p) { - sync_ctx *ctx = baton; + const md_pubcert_t *pub; + const md_cert_t *cert; + md_timeperiod_t certlife, renewal; + int i; + apr_time_t renew_at = 0; + apr_status_t rv; + + if (md->state == MD_S_INCOMPLETE) return apr_time_now(); + for (i = 0; i < md_cert_count(md); ++i) { + rv = md_reg_get_pubcert(&pub, reg, md, i, p); + if (APR_STATUS_IS_ENOENT(rv)) return apr_time_now(); + if (APR_SUCCESS == rv) { + cert = APR_ARRAY_IDX(pub->certs, 0, const md_cert_t*); + certlife.start = md_cert_get_not_before(cert); + certlife.end = md_cert_get_not_after(cert); + + renewal = md_timeperiod_slice_before_end(&certlife, md->renew_window); + if (md_log_is_level(p, MD_LOG_TRACE1)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, p, + "md[%s]: certificate(%d) valid[%s] renewal[%s]", + md->name, i, + md_timeperiod_print(p, &certlife), + md_timeperiod_print(p, &renewal)); + } + + if (renew_at == 0 || renewal.start < renew_at) { + renew_at = renewal.start; + } + } + } + return renew_at; +} - (void)store; - (void)ptemp; - APR_ARRAY_PUSH(ctx->store_mds, const md_t*) = md_clone(ctx->p, md); - return 1; +int md_reg_should_renew(md_reg_t *reg, const md_t *md, apr_pool_t *p) +{ + apr_time_t renew_at; + + renew_at = md_reg_renew_at(reg, md, p); + return renew_at && (renew_at <= apr_time_now()); } -static apr_status_t read_store_mds(md_reg_t *reg, sync_ctx *ctx) +int md_reg_should_warn(md_reg_t *reg, const md_t *md, apr_pool_t *p) { - int rv; + const md_pubcert_t *pub; + const md_cert_t *cert; + md_timeperiod_t certlife, warn; + int i; + apr_status_t 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; + if (md->state == MD_S_INCOMPLETE) return 0; + for (i = 0; i < md_cert_count(md); ++i) { + rv = md_reg_get_pubcert(&pub, reg, md, i, p); + if (APR_STATUS_IS_ENOENT(rv)) return 0; + if (APR_SUCCESS == rv) { + cert = APR_ARRAY_IDX(pub->certs, 0, const md_cert_t*); + certlife.start = md_cert_get_not_before(cert); + certlife.end = md_cert_get_not_after(cert); + + warn = md_timeperiod_slice_before_end(&certlife, md->warn_window); + if (md_log_is_level(p, MD_LOG_TRACE1)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, p, + "md[%s]: certificate(%d) life[%s] warn[%s]", + md->name, i, + md_timeperiod_print(p, &certlife), + md_timeperiod_print(p, &warn)); + } + if (md_timeperiod_has_started(&warn, apr_time_now())) { + return 1; + } + } } - return rv; + return 0; } +/**************************************************************************************************/ +/* syncing */ + 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; + if (reg->domains_frozen) return APR_EACCES; reg->can_http = can_http; reg->can_https = can_https; @@ -676,329 +763,561 @@ apr_status_t md_reg_set_props(md_reg_t *reg, apr_pool_t *p, int can_http, int ca } 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 + +static md_t *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; +} + +typedef struct { + apr_pool_t *p; + apr_array_header_t *master_mds; + apr_array_header_t *store_names; + apr_array_header_t *maybe_new_mds; + apr_array_header_t *new_mds; + apr_array_header_t *unassigned_mds; +} sync_ctx_v2; + +static int iter_add_name(void *baton, const char *dir, const char *name, + md_store_vtype_t vtype, void *value, apr_pool_t *ptemp) +{ + sync_ctx_v2 *ctx = baton; + + (void)dir; + (void)value; + (void)ptemp; + (void)vtype; + APR_ARRAY_PUSH(ctx->store_names, const char*) = apr_pstrdup(ctx->p, name); + return APR_SUCCESS; +} + +/* A better scaling version: + * 1. The consistency of the MDs in 'master_mds' has already been verified. E.g. + * that no domain lists overlap etc. + * 2. All MD storage that exists will be overwritten by the settings we have. + * And "exists" meaning that "store/MD_SG_DOMAINS/name" exists. + * 3. For MDs that have no directory in "store/MD_SG_DOMAINS", we load all MDs + * outside the list of known names from MD_SG_DOMAINS. In this list, we + * look for the MD with the most domain overlap. + * - if we find it, we assume this is a rename and move the old MD to the new name. + * - if not, MD is completely new. + * 4. Any MD in store that does not match the "master_mds" will just be left as is. */ -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_sync_start(md_reg_t *reg, apr_array_header_t *master_mds, apr_pool_t *p) { - sync_ctx ctx; + sync_ctx_v2 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_t *md, *oldmd; + const char *name; + int i, idx; - 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); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "sync MDs, start"); + + ctx.p = p; + ctx.master_mds = master_mds; + ctx.store_names = apr_array_make(p, master_mds->nelts + 100, sizeof(const char*)); + ctx.maybe_new_mds = apr_array_make(p, master_mds->nelts, sizeof(md_t*)); + ctx.new_mds = apr_array_make(p, master_mds->nelts, sizeof(md_t*)); + ctx.unassigned_mds = apr_array_make(p, master_mds->nelts, sizeof(md_t*)); + + rv = md_store_iter_names(iter_add_name, &ctx, reg->store, p, MD_SG_DOMAINS, "*"); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "listing existing store MD names"); + goto leave; + } + + /* Get all MDs that are not already present in store */ + for (i = 0; i < ctx.master_mds->nelts; ++i) { + md = APR_ARRAY_IDX(ctx.master_mds, i, md_t*); + idx = md_array_str_index(ctx.store_names, md->name, 0, 1); + if (idx < 0) { + APR_ARRAY_PUSH(ctx.maybe_new_mds, md_t*) = md; + md_array_remove_at(ctx.store_names, idx); + } + } + + if (ctx.maybe_new_mds->nelts == 0) goto leave; /* none new */ + if (ctx.store_names->nelts == 0) goto leave; /* all new */ + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "sync MDs, %d potentially new MDs detected, looking for renames among " + "the %d unassigned store domains", (int)ctx.maybe_new_mds->nelts, + (int)ctx.store_names->nelts); + for (i = 0; i < ctx.store_names->nelts; ++i) { + name = APR_ARRAY_IDX(ctx.store_names, i, const char*); + if (APR_SUCCESS == md_load(reg->store, MD_SG_DOMAINS, name, &md, p)) { + APR_ARRAY_PUSH(ctx.unassigned_mds, md_t*) = md; + } + } + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "sync MDs, %d MDs maybe new, checking store", (int)ctx.maybe_new_mds->nelts); + for (i = 0; i < ctx.maybe_new_mds->nelts; ++i) { + md = APR_ARRAY_IDX(ctx.maybe_new_mds, i, md_t*); + oldmd = find_closest_match(ctx.unassigned_mds, md); + if (oldmd) { + /* found the rename, move the domains and possible staging directory */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "sync MDs, found MD %s under previous name %s", md->name, oldmd->name); + rv = md_store_rename(reg->store, p, MD_SG_DOMAINS, oldmd->name, md->name); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "sync MDs, renaming MD %s to %s failed", oldmd->name, md->name); + /* ignore it? */ } + md_store_rename(reg->store, p, MD_SG_STAGING, oldmd->name, md->name); + md_array_remove(ctx.unassigned_mds, oldmd); + } + else { + APR_ARRAY_PUSH(ctx.new_mds, md_t*) = md; } } - else { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "loading mds"); + +leave: + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, + "sync MDs, %d existing, %d moved, %d new.", + (int)ctx.master_mds->nelts - ctx.maybe_new_mds->nelts, + (int)ctx.maybe_new_mds->nelts - ctx.new_mds->nelts, + (int)ctx.new_mds->nelts); + return rv; +} + +/** + * Finish syncing an MD with the store. + * 1. if there are changed properties (or if the MD is new), save it. + * 2. read any existing certificate and init the state of the memory MD + */ +apr_status_t md_reg_sync_finish(md_reg_t *reg, md_t *md, apr_pool_t *p, apr_pool_t *ptemp) +{ + md_t *old; + apr_status_t rv; + int changed = 1; + md_proto_t *proto; + + if (!md->ca_proto) { + md->ca_proto = MD_PROTO_ACME; } + proto = apr_hash_get(reg->protos, md->ca_proto, (apr_ssize_t)strlen(md->ca_proto)); + if (!proto) { + rv = APR_ENOTIMPL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, + "[%s] uses unknown CA protocol '%s'", + md->name, md->ca_proto); + goto leave; + } + rv = proto->complete_md(md, p); + if (APR_SUCCESS != rv) goto leave; + + rv = state_init(reg, p, md); + if (APR_SUCCESS != rv) goto leave; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "loading md %s", md->name); + if (APR_SUCCESS == md_load(reg->store, MD_SG_DOMAINS, md->name, &old, ptemp)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "loaded md %s", md->name); + /* Some parts are kept from old, lacking new values */ + if ((!md->contacts || apr_is_empty_array(md->contacts)) && old->contacts) { + md->contacts = md_array_str_clone(p, old->contacts); + } + if (md->ca_challenges && old->ca_challenges) { + if (!md_array_str_eq(md->ca_challenges, old->ca_challenges, 0)) { + md->ca_challenges = md_array_str_compact(p, md->ca_challenges, 0); + } + } + if (!md->ca_effective && old->ca_effective) { + md->ca_effective = apr_pstrdup(p, old->ca_effective); + } + if (!md->ca_account && old->ca_account) { + md->ca_account = apr_pstrdup(p, old->ca_account); + } + + /* if everything remains the same, spare the write back */ + if (!MD_VAL_UPDATE(md, old, state) + && md_array_str_eq(md->ca_urls, old->ca_urls, 0) + && !MD_SVAL_UPDATE(md, old, ca_proto) + && !MD_SVAL_UPDATE(md, old, ca_agreement) + && !MD_VAL_UPDATE(md, old, transitive) + && md_equal_domains(md, old, 1) + && !MD_VAL_UPDATE(md, old, renew_mode) + && md_timeslice_eq(md->renew_window, old->renew_window) + && md_timeslice_eq(md->warn_window, old->warn_window) + && md_pkeys_spec_eq(md->pks, old->pks) + && !MD_VAL_UPDATE(md, old, require_https) + && !MD_VAL_UPDATE(md, old, must_staple) + && md_array_str_eq(md->acme_tls_1_domains, old->acme_tls_1_domains, 0) + && !MD_VAL_UPDATE(md, old, stapling) + && md_array_str_eq(md->contacts, old->contacts, 0) + && md_array_str_eq(md->cert_files, old->cert_files, 0) + && md_array_str_eq(md->pkey_files, old->pkey_files, 0) + && md_array_str_eq(md->ca_challenges, old->ca_challenges, 0)) { + changed = 0; + } + } + if (changed) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "saving md %s", md->name); + rv = md_save(reg->store, ptemp, MD_SG_DOMAINS, md, 0); + } +leave: + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "sync MDs, finish done"); return rv; } apr_status_t md_reg_remove(md_reg_t *reg, apr_pool_t *p, const char *name, int archive) { + if (reg->domains_frozen) return APR_EACCES; return md_store_move(reg->store, p, MD_SG_DOMAINS, MD_SG_ARCHIVE, name, archive); } +typedef struct { + md_reg_t *reg; + apr_pool_t *p; + apr_array_header_t *mds; +} cleanup_challenge_ctx; + +static apr_status_t cleanup_challenge_inspector(void *baton, const char *dir, const char *name, + md_store_vtype_t vtype, void *value, + apr_pool_t *ptemp) +{ + cleanup_challenge_ctx *ctx = baton; + const md_t *md; + int i, used; + apr_status_t rv; + + (void)value; + (void)vtype; + (void)dir; + for (used = 0, i = 0; i < ctx->mds->nelts && !used; ++i) { + md = APR_ARRAY_IDX(ctx->mds, i, const md_t *); + used = !strcmp(name, md->name); + } + if (!used) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, + "challenges/%s: not in use, purging", name); + rv = md_store_purge(ctx->reg->store, ctx->p, MD_SG_CHALLENGES, name); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, ptemp, + "challenges/%s: unable to purge", name); + } + } + return APR_SUCCESS; +} + +apr_status_t md_reg_cleanup_challenges(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, + apr_array_header_t *mds) +{ + apr_status_t rv; + cleanup_challenge_ctx ctx; + + (void)p; + ctx.reg = reg; + ctx.p = ptemp; + ctx.mds = mds; + rv = md_store_iter_names(cleanup_challenge_inspector, &ctx, reg->store, ptemp, + MD_SG_CHALLENGES, "*"); + return rv; +} + /**************************************************************************************************/ /* 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) +static apr_status_t run_init(void *baton, apr_pool_t *p, ...) { - apr_status_t rv = APR_SUCCESS; + va_list ap; + md_reg_t *reg = baton; + const md_t *md; + md_proto_driver_t *driver, **pdriver; + md_result_t *result; + apr_table_t *env; + const char *s; + int preload; + + (void)p; + va_start(ap, p); + pdriver = va_arg(ap, md_proto_driver_t **); + md = va_arg(ap, const md_t *); + preload = va_arg(ap, int); + env = va_arg(ap, apr_table_t *); + result = va_arg(ap, md_result_t *); + va_end(ap); + + *pdriver = driver = apr_pcalloc(p, sizeof(*driver)); - /* 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->env = env? apr_table_copy(p, env) : apr_table_make(p, 10); driver->reg = reg; driver->store = md_reg_store_get(reg); driver->proxy_url = reg->proxy_url; + driver->ca_file = reg->ca_file; driver->md = md; - driver->reset = reset; + driver->can_http = reg->can_http; + driver->can_https = reg->can_https; + + s = apr_table_get(driver->env, MD_KEY_ACTIVATION_DELAY); + if (!s || APR_SUCCESS != md_duration_parse(&driver->activation_delay, s, "d")) { + driver->activation_delay = 0; + } - return rv; + if (!md->ca_proto) { + md_result_printf(result, APR_EGENERAL, "CA protocol is not defined"); + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "md[%s]: %s", md->name, result->detail); + goto leave; + } + + driver->proto = apr_hash_get(reg->protos, md->ca_proto, (apr_ssize_t)strlen(md->ca_proto)); + if (!driver->proto) { + md_result_printf(result, APR_EGENERAL, "Unknown CA protocol '%s'", md->ca_proto); + goto leave; + } + + if (preload) { + result->status = driver->proto->init_preload(driver, result); + } + else { + result->status = driver->proto->init(driver, result); + } + +leave: + if (APR_SUCCESS != result->status) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, result->status, p, "md[%s]: %s", md->name, + result->detail? result->detail : "<see error log for details>"); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "%s: init done", md->name); + } + return result->status; +} + +static apr_status_t run_test_init(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + const md_t *md; + apr_table_t *env; + md_result_t *result; + md_proto_driver_t *driver; + + (void)p; + md = va_arg(ap, const md_t *); + env = va_arg(ap, apr_table_t *); + result = va_arg(ap, md_result_t *); + + return run_init(baton, ptemp, &driver, md, 0, env, result, NULL); } -static apr_status_t run_stage(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +apr_status_t md_reg_test_init(md_reg_t *reg, const md_t *md, struct apr_table_t *env, + md_result_t *result, apr_pool_t *p) +{ + return md_util_pool_vdo(run_test_init, reg, p, md, env, result, NULL); +} + +static apr_status_t run_renew(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; + int reset, attempt; md_proto_driver_t *driver; - const char *challenge; - apr_time_t *pvalid_from; + apr_table_t *env; apr_status_t rv; + md_result_t *result; (void)p; - proto = va_arg(ap, const md_proto_t *); md = va_arg(ap, const md_t *); - challenge = va_arg(ap, const char *); + env = va_arg(ap, apr_table_t *); 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); + attempt = va_arg(ap, int); + result = va_arg(ap, md_result_t *); - if (APR_SUCCESS == rv && pvalid_from) { - *pvalid_from = driver->stage_valid_from; - } + rv = run_init(reg, ptemp, &driver, md, 0, env, result, NULL); + if (APR_SUCCESS == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "%s: run staging", md->name); + driver->reset = reset; + driver->attempt = attempt; + driver->retry_failover = reg->retry_failover; + rv = driver->proto->renew(driver, result); } 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) +apr_status_t md_reg_renew(md_reg_t *reg, const md_t *md, apr_table_t *env, + int reset, int attempt, + md_result_t *result, 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); + return md_util_pool_vdo(run_renew, reg, p, md, env, reset, attempt, result, NULL); } -static apr_status_t run_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +static apr_status_t run_load_staging(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; + const md_t *md; md_proto_driver_t *driver; + md_result_t *result; + apr_table_t *env; + md_job_t *job; apr_status_t rv; - name = va_arg(ap, const char *); + /* For the MD, check if something is in the STAGING area. If none is there, + * return that status. Otherwise ask the protocol driver to preload it into + * a new, temporary area. + * If that succeeds, we move the TEMP area over the DOMAINS (causing the + * existing one go to ARCHIVE). + * Finally, we clean up the data from CHALLENGES and STAGING. + */ + md = va_arg(ap, const md_t*); + env = va_arg(ap, apr_table_t*); + result = va_arg(ap, md_result_t*); - 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; + if (APR_STATUS_IS_ENOENT(rv = md_load(reg->store, MD_SG_STAGING, md->name, NULL, ptemp))) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, ptemp, "%s: nothing staged", md->name); + goto out; } - md = md_reg_get(reg, name, p); - if (!md) { - return APR_ENOENT; - } + rv = run_init(baton, ptemp, &driver, md, 1, env, result, NULL); + if (APR_SUCCESS != rv) goto out; - 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; + apr_hash_set(reg->certs, md->name, (apr_ssize_t)strlen(md->name), NULL); + md_result_activity_setn(result, "preloading staged to tmp"); + rv = driver->proto->preload(driver, MD_SG_TMP, result); + if (APR_SUCCESS != rv) goto out; + + /* If we had a job saved in STAGING, copy it over too */ + job = md_reg_job_make(reg, md->name, ptemp); + if (APR_SUCCESS == md_job_load(job)) { + md_job_set_group(job, MD_SG_TMP); + md_job_save(job, NULL, ptemp); } - 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; + /* swap */ + md_result_activity_setn(result, "moving tmp to become new domains"); + rv = md_store_move(reg->store, p, MD_SG_TMP, MD_SG_DOMAINS, md->name, 1); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, NULL); + goto out; } - driver = apr_pcalloc(ptemp, sizeof(*driver)); - init_proto_driver(driver, proto, reg, md, NULL, 0, ptemp); + md_store_purge(reg->store, p, MD_SG_STAGING, md->name); + md_store_purge(reg->store, p, MD_SG_CHALLENGES, md->name); + md_result_set(result, APR_SUCCESS, "new certificate successfully saved in domains"); + md_event_holler("installed", md->name, job, result, ptemp); + if (job->dirty) md_job_save(job, result, ptemp); + +out: + if (!APR_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ptemp, "%s: load done", md->name); + } + return rv; +} - 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); - } +apr_status_t md_reg_load_staging(md_reg_t *reg, const md_t *md, apr_table_t *env, + md_result_t *result, apr_pool_t *p) +{ + if (reg->domains_frozen) return APR_EACCES; + return md_util_pool_vdo(run_load_staging, reg, p, md, env, result, NULL); +} + +apr_status_t md_reg_load_stagings(md_reg_t *reg, apr_array_header_t *mds, + apr_table_t *env, apr_pool_t *p) +{ + apr_status_t rv = APR_SUCCESS; + md_t *md; + md_result_t *result; + int i; + + for (i = 0; i < mds->nelts; ++i) { + md = APR_ARRAY_IDX(mds, i, md_t *); + result = md_result_md_make(p, md->name); + rv = md_reg_load_staging(reg, md, env, result, p); + if (APR_SUCCESS == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, APLOGNO(10068) + "%s: staged set activated", md->name); + } + else if (!APR_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, APLOGNO(10069) + "%s: error loading staged set", md->name); + } + } + + return rv; +} + +apr_status_t md_reg_lock_global(md_reg_t *reg, apr_pool_t *p) +{ + apr_status_t rv = APR_SUCCESS; + + if (reg->use_store_locks) { + rv = md_store_lock_global(reg->store, p, reg->lock_wait_timeout); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "unable to acquire global store lock"); + } + } + return rv; +} + +void md_reg_unlock_global(md_reg_t *reg, apr_pool_t *p) +{ + if (reg->use_store_locks) { + md_store_unlock_global(reg->store, p); + } +} + +apr_status_t md_reg_freeze_domains(md_reg_t *reg, apr_array_header_t *mds) +{ + apr_status_t rv = APR_SUCCESS; + md_t *md; + const md_pubcert_t *pubcert; + int i, j; + + assert(!reg->domains_frozen); + /* prefill the certs cache for all mds */ + for (i = 0; i < mds->nelts; ++i) { + md = APR_ARRAY_IDX(mds, i, md_t*); + for (j = 0; j < md_cert_count(md); ++j) { + rv = md_reg_get_pubcert(&pubcert, reg, md, i, reg->p); + if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) goto leave; } } - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "%s: load done", md->name); + reg->domains_frozen = 1; +leave: return rv; } -apr_status_t md_reg_load(md_reg_t *reg, const char *name, apr_pool_t *p) +void md_reg_set_renew_window_default(md_reg_t *reg, md_timeslice_t *renew_window) { - return md_util_pool_vdo(run_load, reg, p, name, NULL); + *reg->renew_window = *renew_window; } +void md_reg_set_warn_window_default(md_reg_t *reg, md_timeslice_t *warn_window) +{ + *reg->warn_window = *warn_window; +} + +md_job_t *md_reg_job_make(md_reg_t *reg, const char *mdomain, apr_pool_t *p) +{ + return md_job_make(p, reg->store, MD_SG_STAGING, mdomain, reg->min_delay); +} diff --git a/modules/md/md_reg.h b/modules/md/md_reg.h index d976b7f..58ee16a 100644 --- a/modules/md/md_reg.h +++ b/modules/md/md_reg.h @@ -19,9 +19,12 @@ struct apr_hash_t; struct apr_array_header_t; -struct md_store_t; struct md_pkey_t; struct md_cert_t; +struct md_result_t; +struct md_pkey_spec_t; + +#include "md_store.h" /** * A registry for managed domains with a md_store_t as persistence. @@ -30,13 +33,21 @@ struct md_cert_t; typedef struct md_reg_t md_reg_t; /** - * Initialize the registry, using the pool and loading any existing information - * from the store. + * Create the MD registry, using the pool and store. + * @param preg on APR_SUCCESS, the create md_reg_t + * @param pm memory pool to use for creation + * @param store the store to base on + * @param proxy_url optional URL of a proxy to use for requests + * @param ca_file optioinal CA trust anchor file to use + * @param min_delay minimum delay between renewal attempts for a domain + * @param retry_failover numer of failed renewals attempt to fail over to alternate ACME ca */ -apr_status_t md_reg_init(md_reg_t **preg, apr_pool_t *pm, struct md_store_t *store, - const char *proxy_url); +apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *pm, md_store_t *store, + const char *proxy_url, const char *ca_file, + apr_time_t min_delay, int retry_failover, + int use_store_locks, apr_time_t lock_wait_timeout); -struct md_store_t *md_reg_store_get(md_reg_t *reg); +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); @@ -66,11 +77,6 @@ md_t *md_reg_find_overlap(md_reg_t *reg, const md_t *md, const char **pdomain, a 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); @@ -85,20 +91,22 @@ 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_DOMAINS 0x00001 +#define MD_UPD_CA_URL 0x00002 +#define MD_UPD_CA_PROTO 0x00004 +#define MD_UPD_CA_ACCOUNT 0x00008 +#define MD_UPD_CONTACTS 0x00010 +#define MD_UPD_AGREEMENT 0x00020 +#define MD_UPD_DRIVE_MODE 0x00080 +#define MD_UPD_RENEW_WINDOW 0x00100 +#define MD_UPD_CA_CHALLENGES 0x00200 +#define MD_UPD_PKEY_SPEC 0x00400 +#define MD_UPD_REQUIRE_HTTPS 0x00800 +#define MD_UPD_TRANSITIVE 0x01000 +#define MD_UPD_MUST_STAPLE 0x02000 +#define MD_UPD_PROTO 0x04000 +#define MD_UPD_WARN_WINDOW 0x08000 +#define MD_UPD_STAPLING 0x10000 #define MD_UPD_ALL 0x7FFFFFFF /** @@ -106,26 +114,87 @@ int md_reg_do(md_reg_do_cb *cb, void *baton, md_reg_t *reg, apr_pool_t *p); * 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); + const char *name, const md_t *md, + int fields, int check_consistency); /** - * Get the credentials available for the managed domain md. Returns APR_ENOENT - * when none is available. The returned values are immutable. + * Get the chain of public certificates of the managed domain md, starting with the cert + * of the domain and going up the issuers. Returns APR_ENOENT when not available. */ -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_pubcert(const md_pubcert_t **ppubcert, md_reg_t *reg, + const md_t *md, int i, 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); +/** + * Get the filenames of private key and pubcert of the MD - if they exist. + * @return APR_ENOENT if one or both do not exist. + */ +apr_status_t md_reg_get_cred_files(const char **pkeyfile, const char **pcertfile, + md_reg_t *reg, md_store_group_t group, + const md_t *md, struct md_pkey_spec_t *spec, apr_pool_t *p); /** - * Synchronise the give master mds with the store. + * Synchronize the given 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_sync_start(md_reg_t *reg, apr_array_header_t *master_mds, apr_pool_t *p); + +/** + * Re-compute the state of the MD, given current store contents. + */ +apr_status_t md_reg_sync_finish(md_reg_t *reg, md_t *md, apr_pool_t *p, apr_pool_t *ptemp); + apr_status_t md_reg_remove(md_reg_t *reg, apr_pool_t *p, const char *name, int archive); +/** + * Delete the account from the local store. + */ +apr_status_t md_reg_delete_acct(md_reg_t *reg, apr_pool_t *p, const char *acct_id); + + +/** + * Cleanup any challenges that are no longer in use. + * + * @param reg the registry + * @param p pool for permanent storage + * @param ptemp pool for temporary storage + * @param mds the list of configured MDs + */ +apr_status_t md_reg_cleanup_challenges(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, + apr_array_header_t *mds); + +/** + * Mark all information from group MD_SG_DOMAINS as readonly, deny future modifications + * (MD_SG_STAGING and MD_SG_CHALLENGES remain writeable). For the given MDs, cache + * the public information (MDs themselves and their pubcerts or lack of). + */ +apr_status_t md_reg_freeze_domains(md_reg_t *reg, apr_array_header_t *mds); + +/** + * Return if the certificate of the MD should be renewed. This includes reaching + * the renewal window of an otherwise valid certificate. It return also !0 iff + * no certificate has been obtained yet. + */ +int md_reg_should_renew(md_reg_t *reg, const md_t *md, apr_pool_t *p); + +/** + * Return the timestamp when the certificate should be renewed. A value of 0 + * indicates that that renewal is not configured (see renew_mode). + */ +apr_time_t md_reg_renew_at(md_reg_t *reg, const md_t *md, apr_pool_t *p); + +/** + * Return the timestamp up to which *all* certificates for the MD can be used. + * A value of 0 indicates that there is no certificate. + */ +apr_time_t md_reg_valid_until(md_reg_t *reg, const md_t *md, apr_pool_t *p); + +/** + * Return if a warning should be issued about the certificate expiration. + * This applies the configured warn window to the remaining lifetime of the + * current certiciate. If no certificate is present, this returns 0. + */ +int md_reg_should_warn(md_reg_t *reg, const md_t *md, apr_pool_t *p); + /**************************************************************************************************/ /* protocol drivers */ @@ -133,47 +202,112 @@ typedef struct md_proto_t md_proto_t; typedef struct md_proto_driver_t md_proto_driver_t; +/** + * Operating environment for a protocol driver. This is valid only for the + * duration of one run (init + renew, init + preload). + */ 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; + void *baton; + struct apr_table_t *env; + md_reg_t *reg; + md_store_t *store; + const char *proxy_url; + const char *ca_file; const md_t *md; - void *baton; + + int can_http; + int can_https; int reset; - apr_time_t stage_valid_from; - const char *proxy_url; + int attempt; + int retry_failover; + apr_interval_time_t activation_delay; }; -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); +typedef apr_status_t md_proto_init_cb(md_proto_driver_t *driver, struct md_result_t *result); +typedef apr_status_t md_proto_renew_cb(md_proto_driver_t *driver, struct md_result_t *result); +typedef apr_status_t md_proto_init_preload_cb(md_proto_driver_t *driver, struct md_result_t *result); +typedef apr_status_t md_proto_preload_cb(md_proto_driver_t *driver, + md_store_group_t group, struct md_result_t *result); +typedef apr_status_t md_proto_complete_md_cb(md_t *md, apr_pool_t *p); struct md_proto_t { const char *protocol; md_proto_init_cb *init; - md_proto_stage_cb *stage; + md_proto_renew_cb *renew; + md_proto_init_preload_cb *init_preload; md_proto_preload_cb *preload; + md_proto_complete_md_cb *complete_md; }; +/** + * Run a test initialization of the renew protocol for the given MD. This verifies + * basic parameter settings and is expected to return a description of encountered + * problems in <pmessage> when != APR_SUCCESS. + * A message return is allocated fromt the given pool. + */ +apr_status_t md_reg_test_init(md_reg_t *reg, const md_t *md, struct apr_table_t *env, + struct md_result_t *result, apr_pool_t *p); /** - * Stage a new credentials set for the given managed domain in a separate location - * without interfering with any existing credentials. + * Obtain new credentials for the given managed domain in STAGING. + * @param reg the registry instance + * @param md the mdomain to renew + * @param env global environment of settings + * @param reset != 0 if any previous, partial information should be wiped + * @param attempt the number of attempts made this far (for this md) + * @param result for reporting results of the renewal + * @param p the memory pool to use + * @return APR_SUCCESS if new credentials have been staged successfully */ -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); +apr_status_t md_reg_renew(md_reg_t *reg, const md_t *md, + struct apr_table_t *env, int reset, int attempt, + struct md_result_t *result, 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. + * Load a new set of credentials for the managed domain from STAGING - if it exists. + * This will archive any existing credential data and make the staged set the new one + * in DOMAINS. * If staging is incomplete or missing, the load will fail and all credentials remain * as they are. + * + * @return APR_SUCCESS on loading new data, APR_ENOENT when nothing is staged, error otherwise. + */ +apr_status_t md_reg_load_staging(md_reg_t *reg, const md_t *md, struct apr_table_t *env, + struct md_result_t *result, apr_pool_t *p); + +/** + * Check given MDomains for new data in staging areas and, if it exists, load + * the new credentials. On encountering errors, leave the credentails as + * they are. + */ +apr_status_t md_reg_load_stagings(md_reg_t *reg, apr_array_header_t *mds, + apr_table_t *env, apr_pool_t *p); + +void md_reg_set_renew_window_default(md_reg_t *reg, md_timeslice_t *renew_window); +void md_reg_set_warn_window_default(md_reg_t *reg, md_timeslice_t *warn_window); + +struct md_job_t *md_reg_job_make(md_reg_t *reg, const char *mdomain, apr_pool_t *p); + +/** + * Acquire a cooperative, global lock on registry modifications. Will + * do nothing if locking is not configured. + * + * This will only prevent other children/processes/cluster nodes from + * doing the same and does not protect individual store functions from + * being called without it. + * @param reg the registy + * @param p memory pool to use + * @param max_wait maximum time to wait in order to acquire + * @return APR_SUCCESS when lock was obtained + */ +apr_status_t md_reg_lock_global(md_reg_t *reg, apr_pool_t *p); + +/** + * Realease the global registry lock. Will do nothing if there is no lock. */ -apr_status_t md_reg_load(md_reg_t *reg, const char *name, apr_pool_t *p); +void md_reg_unlock_global(md_reg_t *reg, apr_pool_t *p); #endif /* mod_md_md_reg_h */ diff --git a/modules/md/md_result.c b/modules/md/md_result.c new file mode 100644 index 0000000..64a2f70 --- /dev/null +++ b/modules/md/md_result.c @@ -0,0 +1,285 @@ +/* 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_date.h> +#include <apr_time.h> +#include <apr_strings.h> + +#include "md.h" +#include "md_json.h" +#include "md_log.h" +#include "md_result.h" + +static const char *dup_trim(apr_pool_t *p, const char *s) +{ + char *d = apr_pstrdup(p, s); + if (d) apr_collapse_spaces(d, d); + return d; +} + +md_result_t *md_result_make(apr_pool_t *p, apr_status_t status) +{ + md_result_t *result; + + result = apr_pcalloc(p, sizeof(*result)); + result->p = p; + result->md_name = MD_OTHER; + result->status = status; + return result; +} + +md_result_t *md_result_md_make(apr_pool_t *p, const char *md_name) +{ + md_result_t *result = md_result_make(p, APR_SUCCESS); + result->md_name = md_name; + return result; +} + +void md_result_reset(md_result_t *result) +{ + apr_pool_t *p = result->p; + memset(result, 0, sizeof(*result)); + result->p = p; +} + +static void on_change(md_result_t *result) +{ + if (result->on_change) result->on_change(result, result->on_change_data); +} + +void md_result_activity_set(md_result_t *result, const char *activity) +{ + md_result_activity_setn(result, activity? apr_pstrdup(result->p, activity) : NULL); +} + +void md_result_activity_setn(md_result_t *result, const char *activity) +{ + result->activity = activity; + result->problem = result->detail = NULL; + result->subproblems = NULL; + on_change(result); +} + +void md_result_activity_printf(md_result_t *result, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + md_result_activity_setn(result, apr_pvsprintf(result->p, fmt, ap)); + va_end(ap); +} + +void md_result_set(md_result_t *result, apr_status_t status, const char *detail) +{ + result->status = status; + result->problem = NULL; + result->detail = detail? apr_pstrdup(result->p, detail) : NULL; + result->subproblems = NULL; + on_change(result); +} + +void md_result_problem_set(md_result_t *result, apr_status_t status, + const char *problem, const char *detail, + const md_json_t *subproblems) +{ + result->status = status; + result->problem = dup_trim(result->p, problem); + result->detail = apr_pstrdup(result->p, detail); + result->subproblems = subproblems? md_json_clone(result->p, subproblems) : NULL; + on_change(result); +} + +void md_result_problem_printf(md_result_t *result, apr_status_t status, + const char *problem, const char *fmt, ...) +{ + va_list ap; + + result->status = status; + result->problem = dup_trim(result->p, problem); + + va_start(ap, fmt); + result->detail = apr_pvsprintf(result->p, fmt, ap); + va_end(ap); + result->subproblems = NULL; + on_change(result); +} + +void md_result_printf(md_result_t *result, apr_status_t status, const char *fmt, ...) +{ + va_list ap; + + result->status = status; + va_start(ap, fmt); + result->detail = apr_pvsprintf(result->p, fmt, ap); + va_end(ap); + result->subproblems = NULL; + on_change(result); +} + +void md_result_delay_set(md_result_t *result, apr_time_t ready_at) +{ + result->ready_at = ready_at; + on_change(result); +} + +md_result_t*md_result_from_json(const struct md_json_t *json, apr_pool_t *p) +{ + md_result_t *result; + const char *s; + + result = md_result_make(p, APR_SUCCESS); + result->status = (int)md_json_getl(json, MD_KEY_STATUS, NULL); + result->problem = md_json_dups(p, json, MD_KEY_PROBLEM, NULL); + result->detail = md_json_dups(p, json, MD_KEY_DETAIL, NULL); + result->activity = md_json_dups(p, json, MD_KEY_ACTIVITY, NULL); + s = md_json_dups(p, json, MD_KEY_VALID_FROM, NULL); + if (s && *s) result->ready_at = apr_date_parse_rfc(s); + result->subproblems = md_json_dupj(p, json, MD_KEY_SUBPROBLEMS, NULL); + return result; +} + +struct md_json_t *md_result_to_json(const md_result_t *result, apr_pool_t *p) +{ + md_json_t *json; + char ts[APR_RFC822_DATE_LEN]; + + json = md_json_create(p); + md_json_setl(result->status, json, MD_KEY_STATUS, NULL); + if (result->status > 0) { + char buffer[HUGE_STRING_LEN]; + apr_strerror(result->status, buffer, sizeof(buffer)); + md_json_sets(buffer, json, "status-description", NULL); + } + if (result->problem) md_json_sets(result->problem, json, MD_KEY_PROBLEM, NULL); + if (result->detail) md_json_sets(result->detail, json, MD_KEY_DETAIL, NULL); + if (result->activity) md_json_sets(result->activity, json, MD_KEY_ACTIVITY, NULL); + if (result->ready_at > 0) { + apr_rfc822_date(ts, result->ready_at); + md_json_sets(ts, json, MD_KEY_VALID_FROM, NULL); + } + if (result->subproblems) { + md_json_setj(result->subproblems, json, MD_KEY_SUBPROBLEMS, NULL); + } + return json; +} + +static int str_cmp(const char *s1, const char *s2) +{ + if (s1 == s2) return 0; + if (!s1) return -1; + if (!s2) return 1; + return strcmp(s1, s2); +} + +int md_result_cmp(const md_result_t *r1, const md_result_t *r2) +{ + int n; + if (r1 == r2) return 0; + if (!r1) return -1; + if (!r2) return 1; + if ((n = r1->status - r2->status)) return n; + if ((n = str_cmp(r1->problem, r2->problem))) return n; + if ((n = str_cmp(r1->detail, r2->detail))) return n; + if ((n = str_cmp(r1->activity, r2->activity))) return n; + return (int)(r1->ready_at - r2->ready_at); +} + +void md_result_assign(md_result_t *dest, const md_result_t *src) +{ + dest->status = src->status; + dest->problem = src->problem; + dest->detail = src->detail; + dest->activity = src->activity; + dest->ready_at = src->ready_at; + dest->subproblems = src->subproblems; +} + +void md_result_dup(md_result_t *dest, const md_result_t *src) +{ + dest->status = src->status; + dest->problem = src->problem? dup_trim(dest->p, src->problem) : NULL; + dest->detail = src->detail? apr_pstrdup(dest->p, src->detail) : NULL; + dest->activity = src->activity? apr_pstrdup(dest->p, src->activity) : NULL; + dest->ready_at = src->ready_at; + dest->subproblems = src->subproblems? md_json_clone(dest->p, src->subproblems) : NULL; + on_change(dest); +} + +void md_result_log(md_result_t *result, unsigned int level) +{ + if (md_log_is_level(result->p, (md_log_level_t)level)) { + const char *sep = ""; + const char *msg = ""; + + if (result->md_name) { + msg = apr_psprintf(result->p, "md[%s]", result->md_name); + sep = " "; + } + if (result->activity) { + msg = apr_psprintf(result->p, "%s%swhile[%s]", msg, sep, result->activity); + sep = " "; + } + if (result->problem) { + msg = apr_psprintf(result->p, "%s%sproblem[%s]", msg, sep, result->problem); + sep = " "; + } + if (result->detail) { + msg = apr_psprintf(result->p, "%s%sdetail[%s]", msg, sep, result->detail); + sep = " "; + } + if (result->subproblems) { + msg = apr_psprintf(result->p, "%s%ssubproblems[%s]", msg, sep, + md_json_writep(result->subproblems, result->p, MD_JSON_FMT_COMPACT)); + sep = " "; + } + md_log_perror(MD_LOG_MARK, (md_log_level_t)level, result->status, result->p, "%s", msg); + } +} + +void md_result_on_change(md_result_t *result, md_result_change_cb *cb, void *data) +{ + result->on_change = cb; + result->on_change_data = data; +} + +apr_status_t md_result_raise(md_result_t *result, const char *event, apr_pool_t *p) +{ + if (result->on_raise) return result->on_raise(result, result->on_raise_data, event, p); + return APR_SUCCESS; +} + +void md_result_holler(md_result_t *result, const char *event, apr_pool_t *p) +{ + if (result->on_holler) result->on_holler(result, result->on_holler_data, event, p); +} + +void md_result_on_raise(md_result_t *result, md_result_raise_cb *cb, void *data) +{ + result->on_raise = cb; + result->on_raise_data = data; +} + +void md_result_on_holler(md_result_t *result, md_result_holler_cb *cb, void *data) +{ + result->on_holler = cb; + result->on_holler_data = data; +} diff --git a/modules/md/md_result.h b/modules/md/md_result.h new file mode 100644 index 0000000..e83bdd2 --- /dev/null +++ b/modules/md/md_result.h @@ -0,0 +1,87 @@ +/* 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_result_h +#define mod_md_md_result_h + +struct md_json_t; +struct md_t; + +typedef struct md_result_t md_result_t; + +typedef void md_result_change_cb(md_result_t *result, void *data); +typedef apr_status_t md_result_raise_cb(md_result_t *result, void *data, const char *event, apr_pool_t *p); +typedef void md_result_holler_cb(md_result_t *result, void *data, const char *event, apr_pool_t *p); + +struct md_result_t { + apr_pool_t *p; + const char *md_name; + apr_status_t status; + const char *problem; + const char *detail; + const struct md_json_t *subproblems; + const char *activity; + apr_time_t ready_at; + md_result_change_cb *on_change; + void *on_change_data; + md_result_raise_cb *on_raise; + void *on_raise_data; + md_result_holler_cb *on_holler; + void *on_holler_data; +}; + +md_result_t *md_result_make(apr_pool_t *p, apr_status_t status); +md_result_t *md_result_md_make(apr_pool_t *p, const char *md_name); +void md_result_reset(md_result_t *result); + +void md_result_activity_set(md_result_t *result, const char *activity); +void md_result_activity_setn(md_result_t *result, const char *activity); +void md_result_activity_printf(md_result_t *result, const char *fmt, ...); + +void md_result_set(md_result_t *result, apr_status_t status, const char *detail); +void md_result_problem_set(md_result_t *result, apr_status_t status, + const char *problem, const char *detail, + const struct md_json_t *subproblems); +void md_result_problem_printf(md_result_t *result, apr_status_t status, + const char *problem, const char *fmt, ...); + +#define MD_RESULT_LOG_ID(logno) "urn:org:apache:httpd:log:"logno + +void md_result_printf(md_result_t *result, apr_status_t status, const char *fmt, ...); + +void md_result_delay_set(md_result_t *result, apr_time_t ready_at); + +md_result_t*md_result_from_json(const struct md_json_t *json, apr_pool_t *p); +struct md_json_t *md_result_to_json(const md_result_t *result, apr_pool_t *p); + +int md_result_cmp(const md_result_t *r1, const md_result_t *r2); + +void md_result_assign(md_result_t *dest, const md_result_t *src); +void md_result_dup(md_result_t *dest, const md_result_t *src); + +void md_result_log(md_result_t *result, unsigned int level); + +void md_result_on_change(md_result_t *result, md_result_change_cb *cb, void *data); + +/* events in the context of a result genesis */ + +apr_status_t md_result_raise(md_result_t *result, const char *event, apr_pool_t *p); +void md_result_holler(md_result_t *result, const char *event, apr_pool_t *p); + +void md_result_on_raise(md_result_t *result, md_result_raise_cb *cb, void *data); +void md_result_on_holler(md_result_t *result, md_result_holler_cb *cb, void *data); + +#endif /* mod_md_md_result_h */ diff --git a/modules/md/md_status.c b/modules/md/md_status.c new file mode 100644 index 0000000..936c653 --- /dev/null +++ b/modules/md/md_status.c @@ -0,0 +1,653 @@ +/* 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_acme.h" +#include "md_crypt.h" +#include "md_event.h" +#include "md_log.h" +#include "md_ocsp.h" +#include "md_store.h" +#include "md_result.h" +#include "md_reg.h" +#include "md_util.h" +#include "md_status.h" + +#define MD_STATUS_WITH_SCTS 0 + +/**************************************************************************************************/ +/* certificate status information */ + +static apr_status_t status_get_cert_json(md_json_t **pjson, const md_cert_t *cert, apr_pool_t *p) +{ + const char *finger; + apr_status_t rv = APR_SUCCESS; + md_timeperiod_t valid; + md_json_t *json; + + json = md_json_create(p); + valid.start = md_cert_get_not_before(cert); + valid.end = md_cert_get_not_after(cert); + md_json_set_timeperiod(&valid, json, MD_KEY_VALID, NULL); + md_json_sets(md_cert_get_serial_number(cert, p), json, MD_KEY_SERIAL, NULL); + if (APR_SUCCESS != (rv = md_cert_to_sha256_fingerprint(&finger, cert, p))) goto leave; + md_json_sets(finger, json, MD_KEY_SHA256_FINGERPRINT, NULL); + +#if MD_STATUS_WITH_SCTS + do { + apr_array_header_t *scts; + const char *hex; + const md_sct *sct; + md_json_t *sctj; + int i; + + scts = apr_array_make(p, 5, sizeof(const md_sct*)); + if (APR_SUCCESS == md_cert_get_ct_scts(scts, p, cert)) { + for (i = 0; i < scts->nelts; ++i) { + sct = APR_ARRAY_IDX(scts, i, const md_sct*); + sctj = md_json_create(p); + + apr_rfc822_date(ts, sct->timestamp); + md_json_sets(ts, sctj, "signed", NULL); + md_json_setl(sct->version, sctj, MD_KEY_VERSION, NULL); + md_data_to_hex(&hex, 0, p, sct->logid); + md_json_sets(hex, sctj, "logid", NULL); + md_data_to_hex(&hex, 0, p, sct->signature); + md_json_sets(hex, sctj, "signature", NULL); + md_json_sets(md_nid_get_sname(sct->signature_type_nid), sctj, "signature-type", NULL); + md_json_addj(sctj, json, "scts", NULL); + } + } + while (0); +#endif +leave: + *pjson = (APR_SUCCESS == rv)? json : NULL; + return rv; +} + +static apr_status_t job_loadj(md_json_t **pjson, md_store_group_t group, const char *name, + struct md_reg_t *reg, int with_log, apr_pool_t *p) +{ + apr_status_t rv; + + md_store_t *store = md_reg_store_get(reg); + rv = md_store_load_json(store, group, name, MD_FN_JOB, pjson, p); + if (APR_SUCCESS == rv && !with_log) md_json_del(*pjson, MD_KEY_LOG, NULL); + return rv; +} + +static apr_status_t status_get_cert_json_ex( + md_json_t **pjson, + const md_cert_t *cert, + const md_t *md, + md_reg_t *reg, + md_ocsp_reg_t *ocsp, + int with_logs, + apr_pool_t *p) +{ + md_json_t *certj, *jobj; + md_timeperiod_t ocsp_valid; + md_ocsp_cert_stat_t cert_stat; + apr_status_t rv; + + if (APR_SUCCESS != (rv = status_get_cert_json(&certj, cert, p))) goto leave; + if (md->stapling && ocsp) { + rv = md_ocsp_get_meta(&cert_stat, &ocsp_valid, ocsp, cert, p, md); + if (APR_SUCCESS == rv) { + md_json_sets(md_ocsp_cert_stat_name(cert_stat), certj, MD_KEY_OCSP, MD_KEY_STATUS, NULL); + md_json_set_timeperiod(&ocsp_valid, certj, MD_KEY_OCSP, MD_KEY_VALID, NULL); + } + else if (!APR_STATUS_IS_ENOENT(rv)) goto leave; + rv = APR_SUCCESS; + if (APR_SUCCESS == job_loadj(&jobj, MD_SG_OCSP, md->name, reg, with_logs, p)) { + md_json_setj(jobj, certj, MD_KEY_OCSP, MD_KEY_RENEWAL, NULL); + } + } +leave: + *pjson = (APR_SUCCESS == rv)? certj : NULL; + return rv; +} + +static int get_cert_count(const md_t *md, int from_staging) +{ + if (!from_staging && md->cert_files && md->cert_files->nelts) { + return md->cert_files->nelts; + } + return md_pkeys_spec_count(md->pks); +} + +static const char *get_cert_name(const md_t *md, int i, int from_staging, apr_pool_t *p) +{ + if (!from_staging && md->cert_files && md->cert_files->nelts) { + /* static files configured, not from staging, used index names */ + return apr_psprintf(p, "%d", i); + } + return md_pkey_spec_name(md_pkeys_spec_get(md->pks, i)); +} + +static apr_status_t status_get_certs_json(md_json_t **pjson, apr_array_header_t *certs, + int from_staging, + const md_t *md, md_reg_t *reg, + md_ocsp_reg_t *ocsp, int with_logs, + apr_pool_t *p) +{ + md_json_t *json, *certj; + md_timeperiod_t certs_valid = {0, 0}, valid; + md_cert_t *cert; + int i; + apr_status_t rv = APR_SUCCESS; + + json = md_json_create(p); + for (i = 0; i < get_cert_count(md, from_staging); ++i) { + cert = APR_ARRAY_IDX(certs, i, md_cert_t*); + if (!cert) continue; + + rv = status_get_cert_json_ex(&certj, cert, md, reg, ocsp, with_logs, p); + if (APR_SUCCESS != rv) goto leave; + valid = md_cert_get_valid(cert); + certs_valid = i? md_timeperiod_common(&certs_valid, &valid) : valid; + md_json_setj(certj, json, get_cert_name(md, i, from_staging, p), NULL); + } + + if (certs_valid.start) { + md_json_set_timeperiod(&certs_valid, json, MD_KEY_VALID, NULL); + } +leave: + *pjson = (APR_SUCCESS == rv)? json : NULL; + return rv; +} + +static apr_status_t get_staging_certs_json(md_json_t **pjson, const md_t *md, + md_reg_t *reg, apr_pool_t *p) +{ + md_pkey_spec_t *spec; + int i; + apr_array_header_t *chain, *certs; + const md_cert_t *cert; + apr_status_t rv; + + certs = apr_array_make(p, 5, sizeof(md_cert_t*)); + for (i = 0; i < get_cert_count(md, 1); ++i) { + spec = md_pkeys_spec_get(md->pks, i); + cert = NULL; + rv = md_pubcert_load(md_reg_store_get(reg), MD_SG_STAGING, md->name, spec, &chain, p); + if (APR_SUCCESS == rv) { + cert = APR_ARRAY_IDX(chain, 0, const md_cert_t*); + } + APR_ARRAY_PUSH(certs, const md_cert_t*) = cert; + } + return status_get_certs_json(pjson, certs, 1, md, reg, NULL, 0, p); +} + +static apr_status_t status_get_md_json(md_json_t **pjson, const md_t *md, + md_reg_t *reg, md_ocsp_reg_t *ocsp, + int with_logs, apr_pool_t *p) +{ + md_json_t *mdj, *certsj, *jobj; + int renew; + const md_pubcert_t *pubcert; + const md_cert_t *cert = NULL; + apr_array_header_t *certs; + apr_status_t rv = APR_SUCCESS; + apr_time_t renew_at; + int i; + + mdj = md_to_public_json(md, p); + certs = apr_array_make(p, 5, sizeof(md_cert_t*)); + for (i = 0; i < get_cert_count(md, 0); ++i) { + cert = NULL; + if (APR_SUCCESS == md_reg_get_pubcert(&pubcert, reg, md, i, p)) { + cert = APR_ARRAY_IDX(pubcert->certs, 0, const md_cert_t*); + } + APR_ARRAY_PUSH(certs, const md_cert_t*) = cert; + } + + rv = status_get_certs_json(&certsj, certs, 0, md, reg, ocsp, with_logs, p); + if (APR_SUCCESS != rv) goto leave; + md_json_setj(certsj, mdj, MD_KEY_CERT, NULL); + + renew_at = md_reg_renew_at(reg, md, p); + if (renew_at > 0) { + md_json_set_time(renew_at, mdj, MD_KEY_RENEW_AT, NULL); + } + + md_json_setb(md->stapling, mdj, MD_KEY_STAPLING, NULL); + md_json_setb(md->watched, mdj, MD_KEY_WATCHED, NULL); + renew = md_reg_should_renew(reg, md, p); + if (renew) { + md_json_setb(renew, mdj, MD_KEY_RENEW, NULL); + rv = job_loadj(&jobj, MD_SG_STAGING, md->name, reg, with_logs, p); + if (APR_SUCCESS == rv) { + if (APR_SUCCESS == get_staging_certs_json(&certsj, md, reg, p)) { + md_json_setj(certsj, jobj, MD_KEY_CERT, NULL); + } + md_json_setj(jobj, mdj, MD_KEY_RENEWAL, NULL); + } + else if (APR_STATUS_IS_ENOENT(rv)) rv = APR_SUCCESS; + else goto leave; + } + +leave: + if (APR_SUCCESS != rv) { + md_json_setl(rv, mdj, MD_KEY_ERROR, NULL); + } + *pjson = mdj; + return rv; +} + +apr_status_t md_status_get_md_json(md_json_t **pjson, const md_t *md, + md_reg_t *reg, md_ocsp_reg_t *ocsp, apr_pool_t *p) +{ + return status_get_md_json(pjson, md, reg, ocsp, 1, p); +} + +apr_status_t md_status_get_json(md_json_t **pjson, apr_array_header_t *mds, + md_reg_t *reg, md_ocsp_reg_t *ocsp, apr_pool_t *p) +{ + md_json_t *json, *mdj; + const md_t *md; + int i; + + json = md_json_create(p); + md_json_sets(MOD_MD_VERSION, json, MD_KEY_VERSION, NULL); + for (i = 0; i < mds->nelts; ++i) { + md = APR_ARRAY_IDX(mds, i, const md_t *); + status_get_md_json(&mdj, md, reg, ocsp, 0, p); + md_json_addj(mdj, json, MD_KEY_MDS, NULL); + } + *pjson = json; + return APR_SUCCESS; +} + +/**************************************************************************************************/ +/* drive job persistence */ + +md_job_t *md_job_make(apr_pool_t *p, md_store_t *store, + md_store_group_t group, const char *name, + apr_time_t min_delay) +{ + md_job_t *job = apr_pcalloc(p, sizeof(*job)); + job->group = group; + job->mdomain = apr_pstrdup(p, name); + job->store = store; + job->p = p; + job->max_log = 128; + job->min_delay = min_delay; + return job; +} + +void md_job_set_group(md_job_t *job, md_store_group_t group) +{ + job->group = group; +} + +static void md_job_from_json(md_job_t *job, md_json_t *json, apr_pool_t *p) +{ + const char *s; + + /* not good, this is malloced from a temp pool */ + /*job->mdomain = md_json_gets(json, MD_KEY_NAME, NULL);*/ + job->finished = md_json_getb(json, MD_KEY_FINISHED, NULL); + job->notified = md_json_getb(json, MD_KEY_NOTIFIED, NULL); + job->notified_renewed = md_json_getb(json, MD_KEY_NOTIFIED_RENEWED, NULL); + s = md_json_dups(p, json, MD_KEY_NEXT_RUN, NULL); + if (s && *s) job->next_run = apr_date_parse_rfc(s); + s = md_json_dups(p, json, MD_KEY_LAST_RUN, NULL); + if (s && *s) job->last_run = apr_date_parse_rfc(s); + s = md_json_dups(p, json, MD_KEY_VALID_FROM, NULL); + if (s && *s) job->valid_from = apr_date_parse_rfc(s); + job->error_runs = (int)md_json_getl(json, MD_KEY_ERRORS, NULL); + if (md_json_has_key(json, MD_KEY_LAST, NULL)) { + job->last_result = md_result_from_json(md_json_getcj(json, MD_KEY_LAST, NULL), p); + } + job->log = md_json_getj(json, MD_KEY_LOG, NULL); +} + +static void job_to_json(md_json_t *json, const md_job_t *job, + md_result_t *result, apr_pool_t *p) +{ + char ts[APR_RFC822_DATE_LEN]; + + md_json_sets(job->mdomain, json, MD_KEY_NAME, NULL); + md_json_setb(job->finished, json, MD_KEY_FINISHED, NULL); + md_json_setb(job->notified, json, MD_KEY_NOTIFIED, NULL); + md_json_setb(job->notified_renewed, json, MD_KEY_NOTIFIED_RENEWED, NULL); + if (job->next_run > 0) { + apr_rfc822_date(ts, job->next_run); + md_json_sets(ts, json, MD_KEY_NEXT_RUN, NULL); + } + if (job->last_run > 0) { + apr_rfc822_date(ts, job->last_run); + md_json_sets(ts, json, MD_KEY_LAST_RUN, NULL); + } + if (job->valid_from > 0) { + apr_rfc822_date(ts, job->valid_from); + md_json_sets(ts, json, MD_KEY_VALID_FROM, NULL); + } + md_json_setl(job->error_runs, json, MD_KEY_ERRORS, NULL); + if (!result) result = job->last_result; + if (result) { + md_json_setj(md_result_to_json(result, p), json, MD_KEY_LAST, NULL); + } + if (job->log) md_json_setj(job->log, json, MD_KEY_LOG, NULL); +} + +apr_status_t md_job_load(md_job_t *job) +{ + md_json_t *jprops; + apr_status_t rv; + + rv = md_store_load_json(job->store, job->group, job->mdomain, MD_FN_JOB, &jprops, job->p); + if (APR_SUCCESS == rv) { + md_job_from_json(job, jprops, job->p); + } + return rv; +} + +apr_status_t md_job_save(md_job_t *job, md_result_t *result, apr_pool_t *p) +{ + md_json_t *jprops; + apr_status_t rv; + + jprops = md_json_create(p); + job_to_json(jprops, job, result, p); + rv = md_store_save_json(job->store, p, job->group, job->mdomain, MD_FN_JOB, jprops, 0); + if (APR_SUCCESS == rv) job->dirty = 0; + return rv; +} + +void md_job_log_append(md_job_t *job, const char *type, + const char *status, const char *detail) +{ + md_json_t *entry; + char ts[APR_RFC822_DATE_LEN]; + + entry = md_json_create(job->p); + apr_rfc822_date(ts, apr_time_now()); + md_json_sets(ts, entry, MD_KEY_WHEN, NULL); + md_json_sets(type, entry, MD_KEY_TYPE, NULL); + if (status) md_json_sets(status, entry, MD_KEY_STATUS, NULL); + if (detail) md_json_sets(detail, entry, MD_KEY_DETAIL, NULL); + if (!job->log) job->log = md_json_create(job->p); + md_json_insertj(entry, 0, job->log, MD_KEY_ENTRIES, NULL); + md_json_limita(job->max_log, job->log, MD_KEY_ENTRIES, NULL); + job->dirty = 1; +} + +typedef struct { + md_job_t *job; + const char *type; + md_json_t *entry; + size_t index; +} log_find_ctx; + +static int find_first_log_entry(void *baton, size_t index, md_json_t *entry) +{ + log_find_ctx *ctx = baton; + const char *etype; + + etype = md_json_gets(entry, MD_KEY_TYPE, NULL); + if (etype == ctx->type || (etype && ctx->type && !strcmp(etype, ctx->type))) { + ctx->entry = entry; + ctx->index = index; + return 0; + } + return 1; +} + +md_json_t *md_job_log_get_latest(md_job_t *job, const char *type) + +{ + log_find_ctx ctx; + + memset(&ctx, 0, sizeof(ctx)); + ctx.job = job; + ctx.type = type; + if (job->log) md_json_itera(find_first_log_entry, &ctx, job->log, MD_KEY_ENTRIES, NULL); + return ctx.entry; +} + +apr_time_t md_job_log_get_time_of_latest(md_job_t *job, const char *type) +{ + md_json_t *entry; + const char *s; + + entry = md_job_log_get_latest(job, type); + if (entry) { + s = md_json_gets(entry, MD_KEY_WHEN, NULL); + if (s) return apr_date_parse_rfc(s); + } + return 0; +} + +void md_status_take_stock(md_json_t **pjson, apr_array_header_t *mds, + md_reg_t *reg, apr_pool_t *p) +{ + const md_t *md; + md_job_t *job; + int i, complete, renewing, errored, ready, total; + md_json_t *json; + + json = md_json_create(p); + complete = renewing = errored = ready = total = 0; + for (i = 0; i < mds->nelts; ++i) { + md = APR_ARRAY_IDX(mds, i, const md_t *); + ++total; + switch (md->state) { + case MD_S_COMPLETE: ++complete; /* fall through */ + case MD_S_INCOMPLETE: + if (md_reg_should_renew(reg, md, p)) { + ++renewing; + job = md_reg_job_make(reg, md->name, p); + if (APR_SUCCESS == md_job_load(job)) { + if (job->error_runs > 0 + || (job->last_result && job->last_result->status != APR_SUCCESS)) { + ++errored; + } + else if (job->finished) { + ++ready; + } + } + } + break; + default: ++errored; break; + } + } + md_json_setl(total, json, MD_KEY_TOTAL, NULL); + md_json_setl(complete, json, MD_KEY_COMPLETE, NULL); + md_json_setl(renewing, json, MD_KEY_RENEWING, NULL); + md_json_setl(errored, json, MD_KEY_ERRORED, NULL); + md_json_setl(ready, json, MD_KEY_READY, NULL); + *pjson = json; +} + +typedef struct { + apr_pool_t *p; + md_job_t *job; + md_store_t *store; + md_result_t *last; + apr_time_t last_save; +} md_job_result_ctx; + +static void job_result_update(md_result_t *result, void *data) +{ + md_job_result_ctx *ctx = data; + apr_time_t now; + const char *msg, *sep; + + if (md_result_cmp(ctx->last, result)) { + now = apr_time_now(); + md_result_assign(ctx->last, result); + if (result->activity || result->problem || result->detail) { + msg = sep = ""; + if (result->activity) { + msg = apr_psprintf(result->p, "%s", result->activity); + sep = ": "; + } + if (result->detail) { + msg = apr_psprintf(result->p, "%s%s%s", msg, sep, result->detail); + sep = ", "; + } + if (result->problem) { + msg = apr_psprintf(result->p, "%s%sproblem: %s", msg, sep, result->problem); + sep = " "; + } + md_job_log_append(ctx->job, "progress", NULL, msg); + + if (ctx->store && apr_time_as_msec(now - ctx->last_save) > 500) { + md_job_save(ctx->job, result, ctx->p); + ctx->last_save = now; + } + } + } +} + +static apr_status_t job_result_raise(md_result_t *result, void *data, const char *event, apr_pool_t *p) +{ + md_job_result_ctx *ctx = data; + (void)p; + if (result == ctx->job->observing) { + return md_job_notify(ctx->job, event, result); + } + return APR_SUCCESS; +} + +static void job_result_holler(md_result_t *result, void *data, const char *event, apr_pool_t *p) +{ + md_job_result_ctx *ctx = data; + if (result == ctx->job->observing) { + md_event_holler(event, ctx->job->mdomain, ctx->job, result, p); + } +} + +static void job_observation_start(md_job_t *job, md_result_t *result, md_store_t *store) +{ + md_job_result_ctx *ctx; + + if (job->observing) md_result_on_change(job->observing, NULL, NULL); + job->observing = result; + + ctx = apr_pcalloc(result->p, sizeof(*ctx)); + ctx->p = result->p; + ctx->job = job; + ctx->store = store; + ctx->last = md_result_md_make(result->p, APR_SUCCESS); + md_result_assign(ctx->last, result); + md_result_on_change(result, job_result_update, ctx); + md_result_on_raise(result, job_result_raise, ctx); + md_result_on_holler(result, job_result_holler, ctx); +} + +static void job_observation_end(md_job_t *job) +{ + if (job->observing) md_result_on_change(job->observing, NULL, NULL); + job->observing = NULL; +} + +void md_job_start_run(md_job_t *job, md_result_t *result, md_store_t *store) +{ + job->fatal_error = 0; + job->last_run = apr_time_now(); + job_observation_start(job, result, store); + md_job_log_append(job, "starting", NULL, NULL); +} + +apr_time_t md_job_delay_on_errors(md_job_t *job, int err_count, const char *last_problem) +{ + apr_time_t delay = 0, max_delay = apr_time_from_sec(24*60*60); /* daily */ + unsigned char c; + + if (last_problem && md_acme_problem_is_input_related(last_problem)) { + /* If ACME server reported a problem and that problem indicates that our + * input values, e.g. our configuration, has something wrong, we always + * go to max delay as frequent retries are unlikely to resolve the situation. + * However, we should nevertheless retry daily, bc. it might be that there + * is a bug in the server. Unlikely, but... */ + delay = max_delay; + } + else if (err_count > 0) { + /* back off duration, depending on the errors we encounter in a row */ + delay = job->min_delay << (err_count - 1); + if (delay > max_delay) { + delay = max_delay; + } + } + if (delay > 0) { + /* jitter the delay by +/- 0-50%. + * Background: we see retries of jobs being too regular (e.g. all at midnight), + * possibly cumulating from many installations that restart their Apache at a + * fixed hour. This can contribute to an overload at the CA and a continuation + * of failure. + */ + md_rand_bytes(&c, sizeof(c), job->p); + delay += apr_time_from_sec((apr_time_sec(delay) * (c - 128)) / 256); + } + return delay; +} + +void md_job_end_run(md_job_t *job, md_result_t *result) +{ + if (APR_SUCCESS == result->status) { + job->finished = 1; + job->valid_from = result->ready_at; + job->error_runs = 0; + job->dirty = 1; + md_job_log_append(job, "finished", NULL, NULL); + } + else { + ++job->error_runs; + job->dirty = 1; + job->next_run = apr_time_now() + md_job_delay_on_errors(job, job->error_runs, result->problem); + } + job_observation_end(job); +} + +void md_job_retry_at(md_job_t *job, apr_time_t later) +{ + job->next_run = later; + job->dirty = 1; +} + +apr_status_t md_job_notify(md_job_t *job, const char *reason, md_result_t *result) +{ + apr_status_t rv; + + md_result_set(result, APR_SUCCESS, NULL); + rv = md_event_raise(reason, job->mdomain, job, result, job->p); + job->dirty = 1; + if (APR_SUCCESS == rv && APR_SUCCESS == result->status) { + job->notified = 1; + if (!strcmp("renewed", reason)) { + job->notified_renewed = 1; + } + } + else { + ++job->error_runs; + job->next_run = apr_time_now() + md_job_delay_on_errors(job, job->error_runs, result->problem); + } + return result->status; +} + diff --git a/modules/md/md_status.h b/modules/md/md_status.h new file mode 100644 index 0000000..f4d09bd --- /dev/null +++ b/modules/md/md_status.h @@ -0,0 +1,126 @@ +/* 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_status_h +#define md_status_h + +struct md_json_t; +struct md_reg_t; +struct md_result_t; +struct md_ocsp_reg_t; + +#include "md_store.h" + +/** + * Get a JSON summary of the MD and its status (certificates, jobs, etc.). + */ +apr_status_t md_status_get_md_json(struct md_json_t **pjson, const md_t *md, + struct md_reg_t *reg, struct md_ocsp_reg_t *ocsp, + apr_pool_t *p); + +/** + * Get a JSON summary of all MDs and their status. + */ +apr_status_t md_status_get_json(struct md_json_t **pjson, apr_array_header_t *mds, + struct md_reg_t *reg, struct md_ocsp_reg_t *ocsp, + apr_pool_t *p); + +/** + * Take stock of all MDs given for a short overview. The JSON returned + * will carry integers for MD_KEY_COMPLETE, MD_KEY_RENEWING, + * MD_KEY_ERRORED, MD_KEY_READY and MD_KEY_TOTAL. + */ +void md_status_take_stock(struct md_json_t **pjson, apr_array_header_t *mds, + struct md_reg_t *reg, apr_pool_t *p); + + +typedef struct md_job_t md_job_t; + +struct md_job_t { + md_store_group_t group;/* group where job is persisted */ + const char *mdomain; /* Name of the MD this job is about */ + md_store_t *store; /* store where it is persisted */ + apr_pool_t *p; + apr_time_t next_run; /* Time this job wants to be processed next */ + apr_time_t last_run; /* Time this job ran last (or 0) */ + struct md_result_t *last_result; /* Result from last run */ + int finished; /* true iff the job finished successfully */ + int notified; /* true iff notifications were handled successfully */ + int notified_renewed; /* true iff a 'renewed' notification was handled successfully */ + apr_time_t valid_from; /* at which time the finished job results become valid, 0 if immediate */ + int error_runs; /* Number of errored runs of an unfinished job */ + int fatal_error; /* a fatal error is remedied by retrying */ + md_json_t *log; /* array of log objects with minimum fields + MD_KEY_WHEN (timestamp) and MD_KEY_TYPE (string) */ + apr_size_t max_log; /* max number of log entries, new ones replace oldest */ + int dirty; + struct md_result_t *observing; + apr_time_t min_delay; /* smallest delay a repeated attempt should have */ +}; + +/** + * Create a new job instance for the given MD name. + * Job load/save will work using the name. + */ +md_job_t *md_job_make(apr_pool_t *p, md_store_t *store, + md_store_group_t group, const char *name, + apr_time_t min_delay); + +void md_job_set_group(md_job_t *job, md_store_group_t group); + +/** + * Update the job from storage in <group>/job->mdomain. + */ +apr_status_t md_job_load(md_job_t *job); + +/** + * Update storage from job in <group>/job->mdomain. + */ +apr_status_t md_job_save(md_job_t *job, struct md_result_t *result, apr_pool_t *p); + +/** + * Append to the job's log. Timestamp is automatically added. + * @param type type of log entry + * @param status status of entry (maybe NULL) + * @param detail description of what happened + */ +void md_job_log_append(md_job_t *job, const char *type, + const char *status, const char *detail); + +/** + * Retrieve the latest log entry of a certain type. + */ +md_json_t *md_job_log_get_latest(md_job_t *job, const char *type); + +/** + * Get the time the latest log entry of the given type happened, or 0 if + * none is found. + */ +apr_time_t md_job_log_get_time_of_latest(md_job_t *job, const char *type); + +void md_job_start_run(md_job_t *job, struct md_result_t *result, md_store_t *store); +void md_job_end_run(md_job_t *job, struct md_result_t *result); +void md_job_retry_at(md_job_t *job, apr_time_t later); + +/** + * Given the number of errors and the last problem encountered, + * recommend a delay for the next attempt of job + */ +apr_time_t md_job_delay_on_errors(md_job_t *job, int err_count, const char *last_problem); + +apr_status_t md_job_notify(md_job_t *job, const char *reason, struct md_result_t *result); + +#endif /* md_status_h */ diff --git a/modules/md/md_store.c b/modules/md/md_store.c index a047ff3..59dbd67 100644 --- a/modules/md/md_store.c +++ b/modules/md/md_store.c @@ -55,22 +55,18 @@ static const char *GROUP_NAME[] = { "staging", "archive", "tmp", + "ocsp", NULL }; -const char *md_store_group_name(int group) +const char *md_store_group_name(unsigned int group) { - if ((size_t)group < sizeof(GROUP_NAME)/sizeof(GROUP_NAME[0])) { + if (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, @@ -145,6 +141,33 @@ int md_store_is_newer(md_store_t *store, md_store_group_t group1, md_store_group return store->is_newer(store, group1, group2, name, aspect, p); } +apr_time_t md_store_get_modified(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, apr_pool_t *p) +{ + return store->get_modified(store, group, name, aspect, p); +} + +apr_status_t md_store_iter_names(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern) +{ + return store->iterate_names(inspect, baton, store, p, group, pattern); +} + +apr_status_t md_store_remove_not_modified_since(md_store_t *store, apr_pool_t *p, + apr_time_t modified, + md_store_group_t group, + const char *name, + const char *aspect) +{ + return store->remove_nms(store, p, modified, group, name, aspect); +} + +apr_status_t md_store_rename(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name, const char *to) +{ + return store->rename(store, p, group, name, to); +} + /**************************************************************************************************/ /* convenience */ @@ -231,55 +254,89 @@ typedef struct { 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) +static const char *pk_filename(const char *keyname, const char *base, apr_pool_t *p) { - return md_store_save(store, p, group, name, MD_FN_PRIVKEY, MD_SV_PKEY, pkey, create); + char *s, *t; + /* We also run on various filesystems with difference upper/lower preserve matching + * rules. Normalize the names we use, since private key specifications are basically + * user input. */ + s = (keyname && apr_strnatcasecmp("rsa", keyname))? + apr_pstrcat(p, base, ".", keyname, ".pem", NULL) + : apr_pstrcat(p, base, ".pem", NULL); + for (t = s; *t; t++ ) + *t = (char)apr_tolower(*t); + return s; } -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) +const char *md_pkey_filename(md_pkey_spec_t *spec, apr_pool_t *p) { - return md_store_load(store, group, name, MD_FN_CERT, MD_SV_CERT, (void**)pcert, p); + return pk_filename(md_pkey_spec_name(spec), "privkey", 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) +const char *md_chain_filename(md_pkey_spec_t *spec, apr_pool_t *p) { - return md_store_save(store, p, group, name, MD_FN_CERT, MD_SV_CERT, cert, create); + return pk_filename(md_pkey_spec_name(spec), "pubcert", p); } -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_pkey_load(md_store_t *store, md_store_group_t group, const char *name, + md_pkey_spec_t *spec, md_pkey_t **ppkey, apr_pool_t *p) { - return md_store_load(store, group, name, MD_FN_CHAIN, MD_SV_CHAIN, (void**)pchain, p); + const char *fname = md_pkey_filename(spec, p); + return md_store_load(store, group, name, fname, MD_SV_PKEY, (void**)ppkey, 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_pkey_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *name, + md_pkey_spec_t *spec, struct md_pkey_t *pkey, int create) { - return md_store_save(store, p, group, name, MD_FN_CHAIN, MD_SV_CHAIN, chain, create); + const char *fname = md_pkey_filename(spec, p); + return md_store_save(store, p, group, name, fname, MD_SV_PKEY, pkey, 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) + md_pkey_spec_t *spec, 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); + const char *fname = md_chain_filename(spec, p); + return md_store_load(store, group, name, fname, 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) + md_pkey_spec_t *spec, struct apr_array_header_t *pubcert, int create) +{ + const char *fname = md_chain_filename(spec, p); + return md_store_save(store, p, group, name, fname, MD_SV_CHAIN, pubcert, create); +} + +apr_status_t md_creds_load(md_store_t *store, md_store_group_t group, const char *name, + md_pkey_spec_t *spec, md_credentials_t **pcreds, apr_pool_t *p) +{ + md_credentials_t *creds = apr_pcalloc(p, sizeof(*creds)); + apr_status_t rv; + + creds->spec = spec; + if (APR_SUCCESS != (rv = md_pkey_load(store, group, name, spec, &creds->pkey, p))) { + goto leave; + } + /* chain is optional */ + rv = md_pubcert_load(store, group, name, spec, &creds->chain, p); + if (APR_STATUS_IS_ENOENT(rv)) rv = APR_SUCCESS; +leave: + *pcreds = (APR_SUCCESS == rv)? creds : NULL; + return rv; +} + +apr_status_t md_creds_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, md_credentials_t *creds, int create) { - return md_store_save(store, p, group, name, MD_FN_PUBCERT, MD_SV_CHAIN, pubcert, create); + apr_status_t rv; + + if (APR_SUCCESS != (rv = md_pkey_save(store, p, group, name, creds->spec, creds->pkey, create))) { + goto leave; + } + rv = md_pubcert_save(store, p, group, name, creds->spec, creds->chain, create); +leave: + return rv; } typedef struct { @@ -317,3 +374,12 @@ apr_status_t md_store_md_iter(md_store_md_inspect *inspect, void *baton, md_stor return md_store_iter(insp_md, &ctx, store, p, group, pattern, MD_FN_MD, MD_SV_JSON); } +apr_status_t md_store_lock_global(md_store_t *store, apr_pool_t *p, apr_time_t max_wait) +{ + return store->lock_global(store, p, max_wait); +} + +void md_store_unlock_global(md_store_t *store, apr_pool_t *p) +{ + store->unlock_global(store, p); +} diff --git a/modules/md/md_store.h b/modules/md/md_store.h index 5825189..73c840f 100644 --- a/modules/md/md_store.h +++ b/modules/md/md_store.h @@ -20,101 +20,208 @@ struct apr_array_header_t; struct md_cert_t; struct md_pkey_t; +struct md_pkey_spec_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); +const char *md_store_group_name(unsigned int group); -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); +typedef struct md_store_t md_store_t; -struct md_store_t { - md_store_destroy_cb *destroy; +/** + * A store for domain related data. + * + * The Key for a piece of data is the set of 3 items + * <group> + <domain> + <aspect> + * + * Examples: + * "domains" + "greenbytes.de" + "pubcert.pem" + * "ocsp" + "greenbytes.de" + "ocsp-XXXXX.json" + * + * Storage groups are pre-defined, domain and aspect names can be freely chosen. + * + * Groups reflect use cases and come with security restrictions. The groups + * DOMAINS, ARCHIVE and NONE are only accessible during the startup + * phase of httpd. + * + * Private key are stored unencrypted only in restricted groups. Meaning that certificate + * keys in group DOMAINS are not encrypted, but only readable at httpd start/reload. + * Keys in unrestricted groups are encrypted using a pass phrase generated once and stored + * in NONE. + */ - 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; -}; +/** Value types handled by a store */ +typedef enum { + MD_SV_TEXT, /* plain text, value is (char*) */ + MD_SV_JSON, /* JSON serialization, value is (md_json_t*) */ + MD_SV_CERT, /* PEM x509 certificate, value is (md_cert_t*) */ + MD_SV_PKEY, /* PEM private key, value is (md_pkey_t*) */ + MD_SV_CHAIN, /* list of PEM x509 certificates, value is + (apr_array_header_t*) of (md_cert*) */ +} md_store_vtype_t; + +/** Store storage groups */ +typedef enum { + MD_SG_NONE, /* top level of store, name MUST be NULL in calls */ + MD_SG_ACCOUNTS, /* ACME accounts */ + MD_SG_CHALLENGES, /* challenge response data for a domain */ + MD_SG_DOMAINS, /* live certificates and settings for a domain */ + MD_SG_STAGING, /* staged set of certificate and settings, maybe incomplete */ + MD_SG_ARCHIVE, /* Archived live sets of a domain */ + MD_SG_TMP, /* temporary domain storage */ + MD_SG_OCSP, /* OCSP stapling related domain data */ + MD_SG_COUNT, /* number of storage groups, used in setups */ +} md_store_group_t; + +#define MD_FN_MD "md.json" +#define MD_FN_JOB "job.json" +#define MD_FN_HTTPD_JSON "httpd.json" + +/* The corresponding names for current cert & key files are constructed + * in md_store and md_crypt. + */ -void md_store_destroy(md_store_t *store); +/* These three legacy filenames are only used in md_store_fs to + * upgrade 1.0 directories. They should not be used for any other + * purpose. + */ +#define MD_FN_PRIVKEY "privkey.pem" +#define MD_FN_PUBCERT "pubcert.pem" +#define MD_FN_CERT "cert.pem" +/** + * Load the JSON value at key "group/name/aspect", allocated from pool p. + * @return APR_ENOENT if there is no such value + */ 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); +/** + * Save the JSON value at key "group/name/aspect". If create != 0, fail if there + * already is a value for this key. + */ 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); - +/** + * Load the value of type at key "group/name/aspect", allocated from pool p. Usually, the + * type is expected to be the same as used in saving the value. Some conversions will work, + * others will fail the format. + * @return APR_ENOENT if there is no such value + */ 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); +/** + * Save the JSON value at key "group/name/aspect". If create != 0, fail if there + * already is a value for this key. The provided data MUST be of the correct type. + */ 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); + +/** + * Remove the value stored at key "group/name/aspect". Unless force != 0, a missing + * value will cause the call to fail with APR_ENOENT. + */ 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); +/** + * Remove everything matching key "group/name". + */ apr_status_t md_store_purge(md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *name); +/** + * Remove all items matching the name/aspect patterns that have not been + * modified since the given timestamp. + */ +apr_status_t md_store_remove_not_modified_since(md_store_t *store, apr_pool_t *p, + apr_time_t modified, + md_store_group_t group, + const char *name, + const char *aspect); + +/** + * inspect callback function. Invoked for each matched value. Values allocated from + * ptemp may disappear any time after the call returned. If this function returns + * 0, the iteration is aborted. + */ +typedef int md_store_inspect(void *baton, const char *name, const char *aspect, + md_store_vtype_t vtype, void *value, apr_pool_t *ptemp); +/** + * Iterator over all existing values matching the name pattern. Patterns are evaluated + * using apr_fnmatch() without flags. + */ 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); +/** + * Move everything matching key "from/name" from one group to another. If archive != 0, + * move any existing "to/name" into a new "archive/new_name" location. + */ 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); +/** + * Rename a group member. + */ +apr_status_t md_store_rename(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *name, const char *to); + +/** + * Get the filename of an item stored in "group/name/aspect". The item does + * not have to exist. + */ 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); +/** + * Make a compare on the modification time of "group1/name/aspect" vs. "group2/name/aspect". + */ 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); +/** + * Iterate over all names that exist in a group, e.g. there are items matching + * "group/pattern". The inspect function is called with the name and NULL aspect + * and value. + */ +apr_status_t md_store_iter_names(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern); + +/** + * Get the modification time of the item store under "group/name/aspect". + * @return modification time or 0 if the item does not exist. + */ +apr_time_t md_store_get_modified(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, apr_pool_t *p); + +/** + * Acquire a cooperative, global lock on store modifications. + + * This will only prevent other children/processes/cluster nodes from + * doing the same and does not protect individual store functions from + * being called without it. + * @param store the store + * @param p memory pool to use + * @param max_wait maximum time to wait in order to acquire + * @return APR_SUCCESS when lock was obtained + */ +apr_status_t md_store_lock_global(md_store_t *store, apr_pool_t *p, apr_time_t max_wait); + +/** + * Realease the global store lock. Will do nothing if there is no lock. + */ +void md_store_unlock_global(md_store_t *store, apr_pool_t *p); + /**************************************************************************************************/ /* Storage handling utils */ @@ -134,24 +241,103 @@ apr_status_t md_store_md_iter(md_store_md_inspect *inspect, void *baton, md_stor apr_pool_t *p, md_store_group_t group, const char *pattern); +const char *md_pkey_filename(struct md_pkey_spec_t *spec, apr_pool_t *p); +const char *md_chain_filename(struct md_pkey_spec_t *spec, apr_pool_t *p); + 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); + const char *name, struct md_pkey_spec_t *spec, + 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); + const char *name, struct md_pkey_spec_t *spec, + struct md_pkey_t *pkey, 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); + struct md_pkey_spec_t *spec, 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 md_pkey_spec_t *spec, struct apr_array_header_t *pubcert, int create); +/**************************************************************************************************/ +/* X509 complete credentials */ + +typedef struct md_credentials_t md_credentials_t; +struct md_credentials_t { + struct md_pkey_spec_t *spec; + struct md_pkey_t *pkey; + struct apr_array_header_t *chain; +}; + +apr_status_t md_creds_load(md_store_t *store, md_store_group_t group, const char *name, + struct md_pkey_spec_t *spec, md_credentials_t **pcreds, apr_pool_t *p); +apr_status_t md_creds_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *name, md_credentials_t *creds, int create); + +/**************************************************************************************************/ +/* implementation interface */ + +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 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_names_iter_cb(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern); + +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_rename_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, + const char *from, const char *to); + +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); + +typedef apr_time_t md_store_get_modified_cb(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, apr_pool_t *p); + +typedef apr_status_t md_store_remove_nms_cb(md_store_t *store, apr_pool_t *p, + apr_time_t modified, md_store_group_t group, + const char *name, const char *aspect); +typedef apr_status_t md_store_lock_global_cb(md_store_t *store, apr_pool_t *p, apr_time_t max_wait); +typedef void md_store_unlock_global_cb(md_store_t *store, apr_pool_t *p); + +struct md_store_t { + md_store_save_cb *save; + md_store_load_cb *load; + md_store_remove_cb *remove; + md_store_move_cb *move; + md_store_rename_cb *rename; + md_store_iter_cb *iterate; + md_store_names_iter_cb *iterate_names; + md_store_purge_cb *purge; + md_store_get_fname_cb *get_fname; + md_store_is_newer_cb *is_newer; + md_store_get_modified_cb *get_modified; + md_store_remove_nms_cb *remove_nms; + md_store_lock_global_cb *lock_global; + md_store_unlock_global_cb *unlock_global; +}; + #endif /* mod_md_md_store_h */ diff --git a/modules/md/md_store_fs.c b/modules/md/md_store_fs.c index f399cea..35c24b4 100644 --- a/modules/md/md_store_fs.c +++ b/modules/md/md_store_fs.c @@ -39,6 +39,7 @@ /* file system based implementation of md_store_t */ #define MD_STORE_VERSION 3 +#define MD_FS_LOCK_NAME "store.lock" typedef struct { apr_fileperms_t dir; @@ -55,12 +56,13 @@ struct md_store_fs_t { md_store_fs_cb *event_cb; void *event_baton; - const unsigned char *key; - apr_size_t key_len; + md_data_t key; int plain_pkey[MD_SG_COUNT]; int port_80; int port_443; + + apr_file_t *global_lock; }; #define FS_STORE(store) (md_store_fs_t*)(((char*)store)-offsetof(md_store_fs_t, s)) @@ -78,12 +80,19 @@ static apr_status_t fs_remove(md_store_t *store, md_store_group_t group, 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_remove_nms(md_store_t *store, apr_pool_t *p, + apr_time_t modified, md_store_group_t group, + const char *name, const char *aspect); 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_rename(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *from, const char *to); 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_iterate_names(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern); static apr_status_t fs_get_fname(const char **pfname, md_store_t *store, md_store_group_t group, @@ -92,23 +101,27 @@ static apr_status_t fs_get_fname(const char **pfname, 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_time_t fs_get_modified(md_store_t *store, md_store_group_t group, + const char *name, const char *aspect, apr_pool_t *p); + +static apr_status_t fs_lock_global(md_store_t *store, apr_pool_t *p, apr_time_t max_wait); +static void fs_unlock_global(md_store_t *store, 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))) { + md_data_pinit(&s_fs->key, FS_STORE_KLEN, p); + if (APR_SUCCESS != (rv = md_rand_bytes((unsigned char*)s_fs->key.data, s_fs->key.len, p))) { return rv; } - key64 = md_util_base64url_encode((char *)key, s_fs->key_len, ptemp); + key64 = md_util_base64url_encode(&s_fs->key, 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)); @@ -122,13 +135,12 @@ static apr_status_t rename_pkey(void *baton, apr_pool_t *p, apr_pool_t *ptemp, { 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", + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "renaming %s/%s to %s", dir, name, MD_FN_PRIVKEY); return apr_file_rename(from, to, ptemp); } @@ -143,16 +155,15 @@ static apr_status_t mk_pubcert(void *baton, apr_pool_t *p, apr_pool_t *ptemp, 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) + && APR_STATUS_IS_ENOENT(rv = md_chain_fload(&pubcert, ptemp, fpubcert)) && 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))) { + && MD_OK(md_util_path_merge(&fname, ptemp, dir, "chain.pem", NULL))) { rv = md_chain_fload(&chain, ptemp, fname); if (APR_STATUS_IS_ENOENT(rv)) { @@ -193,10 +204,9 @@ 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; + const char *key64; 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); @@ -215,11 +225,10 @@ static apr_status_t read_store_file(md_store_fs_t *s_fs, const char *fname, 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_util_base64url_decode(&s_fs->key, key64, p); + 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); + s_fs->key.len); return APR_EINVAL; } @@ -237,8 +246,8 @@ static apr_status_t read_store_file(md_store_fs_t *s_fs, const char *fname, 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"); + } + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, "migrated store"); } } return rv; @@ -249,10 +258,14 @@ static apr_status_t setup_store_file(void *baton, apr_pool_t *p, apr_pool_t *pte 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; + /* Added: the encryption of tls-alpn-01 certificate keys is not a security issue + * for these self-signed, short-lived certificates. Having them unencrypted let's + * use pass around the files insteak of an *SSL implementation dependent PKEY_something. + */ + s_fs->plain_pkey[MD_SG_CHALLENGES] = 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))) { @@ -264,7 +277,7 @@ read: 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)) { + && APR_STATUS_IS_EEXIST(rv = init_store_file(s_fs, fname, p, ptemp))) { goto read; } return rv; @@ -274,7 +287,6 @@ apr_status_t md_store_fs_init(md_store_t **pstore, apr_pool_t *p, const char *pa { md_store_fs_t *s_fs; apr_status_t rv = APR_SUCCESS; - MD_CHK_VARS; s_fs = apr_pcalloc(p, sizeof(*s_fs)); @@ -282,11 +294,17 @@ apr_status_t md_store_fs_init(md_store_t **pstore, apr_pool_t *p, const char *pa s_fs->s.save = fs_save; s_fs->s.remove = fs_remove; s_fs->s.move = fs_move; + s_fs->s.rename = fs_rename; s_fs->s.purge = fs_purge; s_fs->s.iterate = fs_iterate; + s_fs->s.iterate_names = fs_iterate_names; s_fs->s.get_fname = fs_get_fname; s_fs->s.is_newer = fs_is_newer; - + s_fs->s.get_modified = fs_get_modified; + s_fs->s.remove_nms = fs_remove_nms; + s_fs->s.lock_global = fs_lock_global; + s_fs->s.unlock_global = fs_unlock_global; + /* 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; @@ -300,20 +318,34 @@ apr_status_t md_store_fs_init(md_store_t **pstore, apr_pool_t *p, const char *pa /* 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; + /* OCSP data is readable by all, no secrets involved */ + s_fs->group_perms[MD_SG_OCSP].dir = MD_FPROT_D_UALL_WREAD; + s_fs->group_perms[MD_SG_OCSP].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 = md_util_is_dir(s_fs->base, p); + if (APR_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, + "store directory does not exist, creating %s", s_fs->base); + rv = apr_dir_make_recursive(s_fs->base, s_fs->def_perms.dir, p); + if (APR_SUCCESS != rv) goto cleanup; 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) goto cleanup; } - - 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); + else if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "not a plain directory, maybe a symlink? %s", s_fs->base); + } + + rv = md_util_pool_vdo(setup_store_file, s_fs, p, NULL); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "init fs store at %s", s_fs->base); } +cleanup: *pstore = (rv == APR_SUCCESS)? &(s_fs->s) : NULL; return rv; } @@ -394,8 +426,8 @@ static void get_pass(const char **ppass, apr_size_t *plen, *plen = 0; } else { - *ppass = (const char *)s_fs->key; - *plen = s_fs->key_len; + *ppass = (const char *)s_fs->key.data; + *plen = s_fs->key.len; } } @@ -446,7 +478,6 @@ static apr_status_t pfs_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l 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 *); @@ -460,7 +491,7 @@ static apr_status_t pfs_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l return rv; } -static apr_status_t dispatch(md_store_fs_t *s_fs, md_store_fs_ev_t ev, int group, +static apr_status_t dispatch(md_store_fs_t *s_fs, md_store_fs_ev_t ev, unsigned int group, const char *fname, apr_filetype_e ftype, apr_pool_t *p) { (void)ev; @@ -477,25 +508,31 @@ static apr_status_t mk_group_dir(const char **pdir, md_store_fs_t *s_fs, { 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; - } - } + *pdir = NULL; + rv = fs_get_dname(pdir, &s_fs->s, group, name, p); + if ((APR_SUCCESS != rv) || (MD_SG_NONE == group)) goto cleanup; + + rv = md_util_is_dir(*pdir, p); + if (APR_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "not a directory, creating %s", *pdir); + rv = apr_dir_make_recursive(*pdir, perms->dir, p); + if (APR_SUCCESS != rv) goto cleanup; + dispatch(s_fs, MD_S_FS_EV_CREATED, group, *pdir, APR_DIR, p); + } + + rv = apr_file_perms_set(*pdir, perms->dir); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "mk_group_dir %s perm set", *pdir); + if (APR_STATUS_IS_ENOTIMPL(rv)) { + rv = APR_SUCCESS; + } +cleanup: + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "mk_group_dir %d %s", + group, (*pdir? *pdir : (name? name : "(null)"))); } - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "mk_group_dir %d %s", group, name); return rv; } @@ -507,7 +544,6 @@ static apr_status_t pfs_is_newer(void *baton, apr_pool_t *p, apr_pool_t *ptemp, 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); @@ -527,7 +563,6 @@ static apr_status_t pfs_is_newer(void *baton, apr_pool_t *p, apr_pool_t *ptemp, 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) { @@ -542,6 +577,44 @@ static int fs_is_newer(md_store_t *store, md_store_group_t group1, md_store_grou return 0; } +static apr_status_t pfs_get_modified(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_fs_t *s_fs = baton; + const char *fname, *name, *aspect; + md_store_group_t group; + apr_finfo_t inf; + apr_time_t *pmtime; + apr_status_t rv; + + (void)p; + group = (md_store_group_t)va_arg(ap, int); + name = va_arg(ap, const char*); + aspect = va_arg(ap, const char*); + pmtime = va_arg(ap, apr_time_t*); + + *pmtime = 0; + if ( MD_OK(fs_get_fname(&fname, &s_fs->s, group, name, aspect, ptemp)) + && MD_OK(apr_stat(&inf, fname, APR_FINFO_MTIME, ptemp))) { + *pmtime = inf.mtime; + } + + return rv; +} + +static apr_time_t fs_get_modified(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); + apr_time_t mtime; + apr_status_t rv; + + rv = md_util_pool_vdo(pfs_get_modified, s_fs, p, group, name, aspect, &mtime, NULL); + if (APR_SUCCESS == rv) { + return mtime; + } + 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; @@ -554,7 +627,6 @@ static apr_status_t pfs_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l 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*); @@ -569,7 +641,7 @@ static apr_status_t pfs_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l && 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); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, ptemp, "storing in %s", fpath); switch (vtype) { case MD_SV_TEXT: rv = (create? md_text_fcreatex(fpath, perms->file, p, value) @@ -612,7 +684,6 @@ static apr_status_t pfs_remove(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va 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); @@ -624,7 +695,7 @@ static apr_status_t pfs_remove(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va 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", + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "start remove of md %s/%s/%s", groupname, name, aspect); if (!MD_OK(apr_stat(&info, dir, APR_FINFO_TYPE, ptemp))) { @@ -673,7 +744,6 @@ static apr_status_t pfs_purge(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_ 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); @@ -685,7 +755,9 @@ static apr_status_t pfs_purge(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_ /* 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); + if (!APR_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, ptemp, "purge %s/%s (%s)", groupname, name, dir); + } return APR_SUCCESS; } @@ -706,7 +778,9 @@ typedef struct { const char *aspect; md_store_vtype_t vtype; md_store_inspect *inspect; + const char *dirname; void *baton; + apr_time_t ts; } inspect_ctx; static apr_status_t insp(void *baton, apr_pool_t *p, apr_pool_t *ptemp, @@ -716,15 +790,38 @@ static apr_status_t insp(void *baton, apr_pool_t *p, apr_pool_t *ptemp, 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; - } + if (APR_SUCCESS == (rv = md_util_path_merge(&fpath, ptemp, dir, name, NULL))) { + rv = fs_fload(&value, ctx->s_fs, fpath, ctx->group, ctx->vtype, p, ptemp); + if (APR_SUCCESS == rv + && !ctx->inspect(ctx->baton, ctx->dirname, name, ctx->vtype, value, p)) { + return APR_EOF; + } + else if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + } + } + return rv; +} + +static apr_status_t insp_dir(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; + const char *fpath; + + (void)ftype; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "inspecting dir at: %s/%s", dir, name); + if (MD_OK(md_util_path_merge(&fpath, p, dir, name, NULL))) { + ctx->dirname = name; + rv = md_util_files_do(insp, ctx, p, fpath, ctx->aspect, NULL); + if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + } + } return rv; } @@ -745,7 +842,97 @@ static apr_status_t fs_iterate(md_store_inspect *inspect, void *baton, md_store_ 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); + rv = md_util_files_do(insp_dir, &ctx, p, ctx.s_fs->base, groupname, pattern, NULL); + + return rv; +} + +static apr_status_t insp_name(void *baton, apr_pool_t *p, apr_pool_t *ptemp, + const char *dir, const char *name, apr_filetype_e ftype) +{ + inspect_ctx *ctx = baton; + + (void)ftype; + (void)p; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "inspecting name at: %s/%s", dir, name); + return ctx->inspect(ctx->baton, dir, name, 0, NULL, ptemp); +} + +static apr_status_t fs_iterate_names(md_store_inspect *inspect, void *baton, md_store_t *store, + apr_pool_t *p, md_store_group_t group, const char *pattern) +{ + const char *groupname; + apr_status_t rv; + inspect_ctx ctx; + + ctx.s_fs = FS_STORE(store); + ctx.group = group; + ctx.pattern = pattern; + ctx.inspect = inspect; + ctx.baton = baton; + groupname = md_store_group_name(group); + + rv = md_util_files_do(insp_name, &ctx, p, ctx.s_fs->base, groupname, pattern, NULL); + + return rv; +} + +static apr_status_t remove_nms_file(void *baton, apr_pool_t *p, apr_pool_t *ptemp, + const char *dir, const char *name, apr_filetype_e ftype) +{ + inspect_ctx *ctx = baton; + const char *fname; + apr_finfo_t inf; + apr_status_t rv = APR_SUCCESS; + + (void)p; + if (APR_DIR == ftype) goto leave; + if (APR_SUCCESS != (rv = md_util_path_merge(&fname, ptemp, dir, name, NULL))) goto leave; + if (APR_SUCCESS != (rv = apr_stat(&inf, fname, APR_FINFO_MTIME, ptemp))) goto leave; + if (inf.mtime >= ctx->ts) goto leave; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "remove_nms file: %s/%s", dir, name); + rv = apr_file_remove(fname, ptemp); + +leave: + return rv; +} + +static apr_status_t remove_nms_dir(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; + const char *fpath; + + (void)ftype; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "remove_nms dir at: %s/%s", dir, name); + if (MD_OK(md_util_path_merge(&fpath, p, dir, name, NULL))) { + ctx->dirname = name; + rv = md_util_files_do(remove_nms_file, ctx, p, fpath, ctx->aspect, NULL); + if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + } + } + return rv; +} + +static apr_status_t fs_remove_nms(md_store_t *store, apr_pool_t *p, + apr_time_t modified, md_store_group_t group, + const char *name, const char *aspect) +{ + const char *groupname; + apr_status_t rv; + inspect_ctx ctx; + + ctx.s_fs = FS_STORE(store); + ctx.group = group; + ctx.pattern = name; + ctx.aspect = aspect; + ctx.ts = modified; + groupname = md_store_group_name(group); + + rv = md_util_files_do(remove_nms_dir, &ctx, p, ctx.s_fs->base, groupname, name, NULL); return rv; } @@ -760,7 +947,6 @@ static apr_status_t pfs_move(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l 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); @@ -802,7 +988,7 @@ static apr_status_t pfs_move(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l 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", + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ptemp, "using archive dir: %s", narch_dir); break; } @@ -817,7 +1003,7 @@ static apr_status_t pfs_move(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l 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", + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ptemp, "using archive dir: %s", narch_dir); break; } @@ -844,12 +1030,12 @@ static apr_status_t pfs_move(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l } 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", + 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", + 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; @@ -860,7 +1046,7 @@ static apr_status_t pfs_move(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l } 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", + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", from_dir, to_dir); goto out; } @@ -881,3 +1067,103 @@ static apr_status_t fs_move(md_store_t *store, apr_pool_t *p, md_store_fs_t *s_fs = FS_STORE(store); return md_util_pool_vdo(pfs_move, s_fs, p, from, to, name, archive, NULL); } + +static apr_status_t pfs_rename(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_store_fs_t *s_fs = baton; + const char *group_name, *from_dir, *to_dir; + md_store_group_t group; + const char *from, *to; + apr_status_t rv; + + (void)p; + group = (md_store_group_t)va_arg(ap, int); + from = va_arg(ap, const char*); + to = va_arg(ap, const char*); + + group_name = md_store_group_name(group); + if ( !MD_OK(md_util_path_merge(&from_dir, ptemp, s_fs->base, group_name, from, NULL)) + || !MD_OK(md_util_path_merge(&to_dir, ptemp, s_fs->base, group_name, to, NULL))) { + goto out; + } + + if (APR_SUCCESS != (rv = apr_file_rename(from_dir, to_dir, ptemp)) + && !APR_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", + from_dir, to_dir); + goto out; + } +out: + return rv; +} + +static apr_status_t fs_rename(md_store_t *store, apr_pool_t *p, + md_store_group_t group, const char *from, const char *to) +{ + md_store_fs_t *s_fs = FS_STORE(store); + return md_util_pool_vdo(pfs_rename, s_fs, p, group, from, to, NULL); +} + +static apr_status_t fs_lock_global(md_store_t *store, apr_pool_t *p, apr_time_t max_wait) +{ + md_store_fs_t *s_fs = FS_STORE(store); + apr_status_t rv; + const char *lpath; + apr_time_t end; + + if (s_fs->global_lock) { + rv = APR_EEXIST; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "already locked globally"); + goto cleanup; + } + + rv = md_util_path_merge(&lpath, p, s_fs->base, MD_FS_LOCK_NAME, NULL); + if (APR_SUCCESS != rv) goto cleanup; + end = apr_time_now() + max_wait; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, + "acquire global lock: %s", lpath); + while (apr_time_now() < end) { + rv = apr_file_open(&s_fs->global_lock, lpath, + (APR_FOPEN_WRITE|APR_FOPEN_CREATE), + MD_FPROT_F_UALL_GREAD, p); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p, + "unable to create/open lock file: %s", + lpath); + goto next_try; + } + rv = apr_file_lock(s_fs->global_lock, + APR_FLOCK_EXCLUSIVE|APR_FLOCK_NONBLOCK); + if (APR_SUCCESS == rv) { + goto cleanup; + } + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p, + "unable to obtain lock on: %s", + lpath); + + next_try: + if (s_fs->global_lock) { + apr_file_close(s_fs->global_lock); + s_fs->global_lock = NULL; + } + apr_sleep(apr_time_from_msec(100)); + } + rv = APR_EGENERAL; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p, + "acquire global lock: %s", lpath); + +cleanup: + return rv; +} + +static void fs_unlock_global(md_store_t *store, apr_pool_t *p) +{ + md_store_fs_t *s_fs = FS_STORE(store); + + (void)p; + if (s_fs->global_lock) { + apr_file_close(s_fs->global_lock); + s_fs->global_lock = NULL; + } +} diff --git a/modules/md/md_store_fs.h b/modules/md/md_store_fs.h index 4167c9b..dcdb897 100644 --- a/modules/md/md_store_fs.h +++ b/modules/md/md_store_fs.h @@ -56,7 +56,7 @@ typedef enum { } 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, + md_store_fs_ev_t ev, unsigned int group, const char *fname, apr_filetype_e ftype, apr_pool_t *p); diff --git a/modules/md/md_tailscale.c b/modules/md/md_tailscale.c new file mode 100644 index 0000000..c8d2bad --- /dev/null +++ b/modules/md/md_tailscale.c @@ -0,0 +1,383 @@ +/* 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_hash.h> +#include <apr_uri.h> + +#include "md.h" +#include "md_crypt.h" +#include "md_json.h" +#include "md_http.h" +#include "md_log.h" +#include "md_result.h" +#include "md_reg.h" +#include "md_store.h" +#include "md_util.h" + +#include "md_tailscale.h" + +typedef struct { + apr_pool_t *pool; + md_proto_driver_t *driver; + const char *unix_socket_path; + md_t *md; + apr_array_header_t *chain; + md_pkey_t *pkey; +} ts_ctx_t; + +static apr_status_t ts_init(md_proto_driver_t *d, md_result_t *result) +{ + ts_ctx_t *ts_ctx; + apr_uri_t uri; + const char *ca_url; + apr_status_t rv = APR_SUCCESS; + + md_result_set(result, APR_SUCCESS, NULL); + ts_ctx = apr_pcalloc(d->p, sizeof(*ts_ctx)); + ts_ctx->pool = d->p; + ts_ctx->driver = d; + ts_ctx->chain = apr_array_make(d->p, 5, sizeof(md_cert_t *)); + + ca_url = (d->md->ca_urls && !apr_is_empty_array(d->md->ca_urls))? + APR_ARRAY_IDX(d->md->ca_urls, 0, const char*) : NULL; + if (!ca_url) { + ca_url = MD_TAILSCALE_DEF_URL; + } + rv = apr_uri_parse(d->p, ca_url, &uri); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "error parsing CA URL `%s`", ca_url); + goto leave; + } + if (uri.scheme && uri.scheme[0] && strcmp("file", uri.scheme)) { + rv = APR_ENOTIMPL; + md_result_printf(result, rv, "non `file` URLs not supported, CA URL is `%s`", + ca_url); + goto leave; + } + if (uri.hostname && uri.hostname[0] && strcmp("localhost", uri.hostname)) { + rv = APR_ENOTIMPL; + md_result_printf(result, rv, "non `localhost` URLs not supported, CA URL is `%s`", + ca_url); + goto leave; + } + ts_ctx->unix_socket_path = uri.path; + d->baton = ts_ctx; + +leave: + return rv; +} + +static apr_status_t ts_preload_init(md_proto_driver_t *d, md_result_t *result) +{ + return ts_init(d, result); +} + +static apr_status_t ts_preload(md_proto_driver_t *d, + md_store_group_t load_group, md_result_t *result) +{ + apr_status_t rv; + md_t *md; + md_credentials_t *creds; + md_pkey_spec_t *pkspec; + apr_array_header_t *all_creds; + const char *name; + int i; + + name = d->md->name; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: preload start", name); + /* Load data from MD_SG_STAGING and save it into "load_group". + */ + if (APR_SUCCESS != (rv = md_load(d->store, MD_SG_STAGING, name, &md, d->p))) { + md_result_set(result, rv, "loading staged md.json"); + goto leave; + } + + /* tailscale generates one cert+key with key specification being whatever + * it chooses. Use the NULL spec here. + */ + all_creds = apr_array_make(d->p, 5, sizeof(md_credentials_t*)); + pkspec = NULL; + if (APR_SUCCESS != (rv = md_creds_load(d->store, MD_SG_STAGING, name, pkspec, &creds, d->p))) { + md_result_printf(result, rv, "loading staged credentials"); + goto leave; + } + if (!creds->chain) { + rv = APR_ENOENT; + md_result_printf(result, rv, "no certificate in staged credentials"); + goto leave; + } + if (APR_SUCCESS != (rv = md_check_cert_and_pkey(creds->chain, creds->pkey))) { + md_result_printf(result, rv, "certificate and private key do not match in staged credentials"); + goto leave; + } + APR_ARRAY_PUSH(all_creds, md_credentials_t*) = creds; + + md_result_activity_setn(result, "purging store tmp space"); + rv = md_store_purge(d->store, d->p, load_group, name); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, NULL); + goto leave; + } + + md_result_activity_setn(result, "saving staged md/privkey/pubcert"); + if (APR_SUCCESS != (rv = md_save(d->store, d->p, load_group, md, 1))) { + md_result_set(result, rv, "writing md.json"); + goto leave; + } + + for (i = 0; i < all_creds->nelts; ++i) { + creds = APR_ARRAY_IDX(all_creds, i, md_credentials_t*); + if (APR_SUCCESS != (rv = md_creds_save(d->store, d->p, load_group, name, creds, 1))) { + md_result_printf(result, rv, "writing credentials #%d", i); + goto leave; + } + } + + md_result_set(result, APR_SUCCESS, "saved staged data successfully"); + +leave: + md_result_log(result, MD_LOG_DEBUG); + return rv; +} + +static apr_status_t rv_of_response(const md_http_response_t *res) +{ + switch (res->status) { + case 200: + return APR_SUCCESS; + case 400: + return APR_EINVAL; + case 401: /* sectigo returns this instead of 403 */ + case 403: + return APR_EACCES; + case 404: + return APR_ENOENT; + default: + return APR_EGENERAL; + } + return APR_SUCCESS; +} + +static apr_status_t on_get_cert(const md_http_response_t *res, void *baton) +{ + ts_ctx_t *ts_ctx = baton; + apr_status_t rv; + + rv = rv_of_response(res); + if (APR_SUCCESS != rv) goto leave; + apr_array_clear(ts_ctx->chain); + rv = md_cert_chain_read_http(ts_ctx->chain, ts_ctx->pool, res); + if (APR_SUCCESS != rv) goto leave; + +leave: + return rv; +} + +static apr_status_t on_get_key(const md_http_response_t *res, void *baton) +{ + ts_ctx_t *ts_ctx = baton; + apr_status_t rv; + + rv = rv_of_response(res); + if (APR_SUCCESS != rv) goto leave; + rv = md_pkey_read_http(&ts_ctx->pkey, ts_ctx->pool, res); + if (APR_SUCCESS != rv) goto leave; + +leave: + return rv; +} + +static apr_status_t ts_renew(md_proto_driver_t *d, md_result_t *result) +{ + const char *name, *domain, *url; + apr_status_t rv = APR_ENOENT; + ts_ctx_t *ts_ctx = d->baton; + md_http_t *http; + const md_pubcert_t *pubcert; + md_cert_t *old_cert, *new_cert; + int reset_staging = d->reset; + + /* "renewing" the certificate from tailscale. Since tailscale has its + * own ideas on when to do this, we can only inspect the certificate + * it gives us and see if it is different from the current one we have. + * (if we have any. first time, lacking a cert, any it gives us is + * considered as 'renewed'.) + */ + name = d->md->name; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: renewing cert", name); + + /* When not explicitly told to reset, we check the existing data. If + * it is incomplete or old, we trigger the reset for a clean start. */ + if (!reset_staging) { + md_result_activity_setn(result, "Checking staging area"); + rv = md_load(d->store, MD_SG_STAGING, d->md->name, &ts_ctx->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; + } + } + + if (reset_staging) { + md_result_activity_setn(result, "Resetting staging area"); + /* reset the staging area for this domain */ + rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "%s: reset staging area", d->md->name); + if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) { + md_result_printf(result, rv, "resetting staging area"); + goto leave; + } + rv = APR_SUCCESS; + ts_ctx->md = NULL; + } + + if (!ts_ctx->md || !md_array_str_eq(ts_ctx->md->ca_urls, d->md->ca_urls, 1)) { + md_result_activity_printf(result, "Resetting staging for %s", d->md->name); + /* re-initialize staging */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: setup staging", d->md->name); + md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); + ts_ctx->md = md_copy(d->p, d->md); + rv = md_save(d->store, d->p, MD_SG_STAGING, ts_ctx->md, 0); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Saving MD information in staging area."); + md_result_log(result, MD_LOG_ERR); + goto leave; + } + } + + if (!ts_ctx->unix_socket_path) { + rv = APR_ENOTIMPL; + md_result_set(result, rv, "only unix sockets are supported for tailscale connections"); + goto leave; + } + + rv = md_util_is_unix_socket(ts_ctx->unix_socket_path, d->p); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "tailscale socket not available, may not be up: %s", + ts_ctx->unix_socket_path); + goto leave; + } + + rv = md_http_create(&http, d->p, + apr_psprintf(d->p, "Apache mod_md/%s", MOD_MD_VERSION), + NULL); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, "creating http context"); + goto leave; + } + md_http_set_unix_socket_path(http, ts_ctx->unix_socket_path); + + domain = (d->md->domains->nelts > 0)? + APR_ARRAY_IDX(d->md->domains, 0, const char*) : NULL; + if (!domain) { + rv = APR_EINVAL; + md_result_set(result, rv, "no domain names available"); + } + + url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=crt", + domain); + rv = md_http_GET_perform(http, url, NULL, on_get_cert, ts_ctx); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, "retrieving certificate from tailscale"); + goto leave; + } + if (ts_ctx->chain->nelts <= 0) { + rv = APR_ENOENT; + md_result_set(result, rv, "tailscale returned no certificates"); + goto leave; + } + + /* Got the key and the chain, is it new? */ + rv = md_reg_get_pubcert(&pubcert, d->reg,d->md, 0, d->p); + if (APR_SUCCESS == rv) { + old_cert = APR_ARRAY_IDX(pubcert->certs, 0, md_cert_t*); + new_cert = APR_ARRAY_IDX(ts_ctx->chain, 0, md_cert_t*); + if (md_certs_are_equal(old_cert, new_cert)) { + /* tailscale has not renewed the certificate, yet */ + rv = APR_ENOENT; + md_result_set(result, rv, "tailscale has not renewed the certificate yet"); + /* let's check this daily */ + md_result_delay_set(result, apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY)); + goto leave; + } + } + + /* We have a new certificate (or had none before). + * Get the key and store both in STAGING. + */ + url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=key", + domain); + rv = md_http_GET_perform(http, url, NULL, on_get_key, ts_ctx); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, "retrieving key from tailscale"); + goto leave; + } + + rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, name, NULL, ts_ctx->pkey, 1); + if (APR_SUCCESS != rv) { + md_result_set(result, rv, "saving private key"); + goto leave; + } + + rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, name, + NULL, ts_ctx->chain, 1); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "saving new certificate chain."); + goto leave; + } + + md_result_set(result, APR_SUCCESS, + "A new tailscale certificate has been retrieved successfully and can " + "be used. A graceful server restart is recommended."); + +leave: + md_result_log(result, MD_LOG_DEBUG); + return rv; +} + +static apr_status_t ts_complete_md(md_t *md, apr_pool_t *p) +{ + (void)p; + if (!md->ca_urls) { + md->ca_urls = apr_array_make(p, 3, sizeof(const char *)); + APR_ARRAY_PUSH(md->ca_urls, const char*) = MD_TAILSCALE_DEF_URL; + } + return APR_SUCCESS; +} + + +static md_proto_t TAILSCALE_PROTO = { + MD_PROTO_TAILSCALE, ts_init, ts_renew, + ts_preload_init, ts_preload, ts_complete_md, +}; + +apr_status_t md_tailscale_protos_add(apr_hash_t *protos, apr_pool_t *p) +{ + (void)p; + apr_hash_set(protos, MD_PROTO_TAILSCALE, sizeof(MD_PROTO_TAILSCALE)-1, &TAILSCALE_PROTO); + return APR_SUCCESS; +} diff --git a/modules/md/md_tailscale.h b/modules/md/md_tailscale.h new file mode 100644 index 0000000..67a874d --- /dev/null +++ b/modules/md/md_tailscale.h @@ -0,0 +1,25 @@ +/* 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_tailscale_h +#define mod_md_md_tailscale_h + +#define MD_PROTO_TAILSCALE "tailscale" + +apr_status_t md_tailscale_protos_add(struct apr_hash_t *protos, apr_pool_t *p); + +#endif /* mod_md_md_tailscale_h */ + diff --git a/modules/md/md_time.c b/modules/md/md_time.c new file mode 100644 index 0000000..268ca83 --- /dev/null +++ b/modules/md/md_time.c @@ -0,0 +1,325 @@ +/* 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_time.h> + +#include "md.h" +#include "md_time.h" + +apr_time_t md_timeperiod_length(const md_timeperiod_t *period) +{ + return (period->start < period->end)? (period->end - period->start) : 0; +} + +int md_timeperiod_contains(const md_timeperiod_t *period, apr_time_t time) +{ + return md_timeperiod_has_started(period, time) + && !md_timeperiod_has_ended(period, time); +} + +int md_timeperiod_has_started(const md_timeperiod_t *period, apr_time_t time) +{ + return (time >= period->start); +} + +int md_timeperiod_has_ended(const md_timeperiod_t *period, apr_time_t time) +{ + return (time >= period->start) && (time <= period->end); +} + +apr_interval_time_t md_timeperiod_remaining(const md_timeperiod_t *period, apr_time_t time) +{ + if (time < period->start) return md_timeperiod_length(period); + if (time < period->end) return period->end - time; + return 0; +} + +char *md_timeperiod_print(apr_pool_t *p, const md_timeperiod_t *period) +{ + char tstart[APR_RFC822_DATE_LEN]; + char tend[APR_RFC822_DATE_LEN]; + + apr_rfc822_date(tstart, period->start); + apr_rfc822_date(tend, period->end); + return apr_pstrcat(p, tstart, " - ", tend, NULL); +} + +static const char *duration_print(apr_pool_t *p, int roughly, apr_interval_time_t duration) +{ + const char *s = "", *sep = ""; + long days = (long)(apr_time_sec(duration) / MD_SECS_PER_DAY); + int rem = (int)(apr_time_sec(duration) % MD_SECS_PER_DAY); + + s = roughly? "~" : ""; + if (days > 0) { + s = apr_psprintf(p, "%s%ld days", s, days); + if (roughly) return s; + sep = " "; + } + if (rem > 0) { + int hours = (rem / MD_SECS_PER_HOUR); + rem = (rem % MD_SECS_PER_HOUR); + if (hours > 0) { + s = apr_psprintf(p, "%s%s%d hours", s, sep, hours); + if (roughly) return s; + sep = " "; + } + if (rem > 0) { + int minutes = (rem / 60); + rem = (rem % 60); + if (minutes > 0) { + s = apr_psprintf(p, "%s%s%d minutes", s, sep, minutes); + if (roughly) return s; + sep = " "; + } + if (rem > 0) { + s = apr_psprintf(p, "%s%s%d seconds", s, sep, rem); + if (roughly) return s; + sep = " "; + } + } + } + else if (days == 0) { + s = "0 seconds"; + if (duration != 0) { + s = apr_psprintf(p, "%d ms", (int)apr_time_msec(duration)); + } + } + return s; +} + +const char *md_duration_print(apr_pool_t *p, apr_interval_time_t duration) +{ + return duration_print(p, 0, duration); +} + +const char *md_duration_roughly(apr_pool_t *p, apr_interval_time_t duration) +{ + return duration_print(p, 1, duration); +} + +static const char *duration_format(apr_pool_t *p, apr_interval_time_t duration) +{ + const char *s = "0"; + int units = (int)(apr_time_sec(duration) / MD_SECS_PER_DAY); + int rem = (int)(apr_time_sec(duration) % MD_SECS_PER_DAY); + + if (rem == 0) { + s = apr_psprintf(p, "%dd", units); + } + else { + units = (int)(apr_time_sec(duration) / MD_SECS_PER_HOUR); + rem = (int)(apr_time_sec(duration) % MD_SECS_PER_HOUR); + if (rem == 0) { + s = apr_psprintf(p, "%dh", units); + } + else { + units = (int)(apr_time_sec(duration) / 60); + rem = (int)(apr_time_sec(duration) % 60); + if (rem == 0) { + s = apr_psprintf(p, "%dmi", units); + } + else { + units = (int)(apr_time_sec(duration)); + rem = (int)(apr_time_msec(duration) % 1000); + if (rem == 0) { + s = apr_psprintf(p, "%ds", units); + } + else { + s = apr_psprintf(p, "%dms", (int)(apr_time_msec(duration))); + } + } + } + } + return s; +} + +const char *md_duration_format(apr_pool_t *p, apr_interval_time_t duration) +{ + return duration_format(p, duration); +} + +apr_status_t md_duration_parse(apr_interval_time_t *ptimeout, const char *value, + const char *def_unit) +{ + char *endp; + apr_int64_t n; + + n = apr_strtoi64(value, &endp, 10); + if (errno) { + return errno; + } + if (!endp || !*endp) { + if (!def_unit) def_unit = "s"; + } + else if (endp == value) { + return APR_EINVAL; + } + else { + def_unit = endp; + } + + switch (*def_unit) { + case 'D': + case 'd': + *ptimeout = apr_time_from_sec(n * MD_SECS_PER_DAY); + break; + case 's': + case 'S': + *ptimeout = (apr_interval_time_t) apr_time_from_sec(n); + break; + case 'h': + case 'H': + /* Time is in hours */ + *ptimeout = (apr_interval_time_t) apr_time_from_sec(n * MD_SECS_PER_HOUR); + break; + case 'm': + case 'M': + switch (*(++def_unit)) { + /* Time is in milliseconds */ + case 's': + case 'S': + *ptimeout = (apr_interval_time_t) n * 1000; + break; + /* Time is in minutes */ + case 'i': + case 'I': + *ptimeout = (apr_interval_time_t) apr_time_from_sec(n * 60); + break; + default: + return APR_EGENERAL; + } + break; + default: + return APR_EGENERAL; + } + return APR_SUCCESS; +} + +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) { + return APR_BADARG; + } + *ppercent = (int)n; + return APR_SUCCESS; + } + return APR_EINVAL; +} + +apr_status_t md_timeslice_create(md_timeslice_t **pts, apr_pool_t *p, + apr_interval_time_t norm, apr_interval_time_t len) +{ + md_timeslice_t *ts; + + ts = apr_pcalloc(p, sizeof(*ts)); + ts->norm = norm; + ts->len = len; + *pts = ts; + return APR_SUCCESS; +} + +const char *md_timeslice_parse(md_timeslice_t **pts, apr_pool_t *p, + const char *val, apr_interval_time_t norm) +{ + md_timeslice_t *ts; + int percent = 0; + + *pts = NULL; + if (!val) { + return "cannot parse NULL value"; + } + + ts = apr_pcalloc(p, sizeof(*ts)); + if (md_duration_parse(&ts->len, val, "d") == APR_SUCCESS) { + *pts = ts; + return NULL; + } + else { + switch (percentage_parse(val, &percent)) { + case APR_SUCCESS: + ts->norm = norm; + ts->len = apr_time_from_sec((apr_time_sec(norm) * percent / 100L)); + *pts = ts; + return NULL; + case APR_BADARG: + return "percent must be less than 100"; + } + } + return "has unrecognized format"; +} + +const char *md_timeslice_format(const md_timeslice_t *ts, apr_pool_t *p) { + if (ts->norm > 0) { + int percent = (int)(((long)apr_time_sec(ts->len)) * 100L + / ((long)apr_time_sec(ts->norm))); + return apr_psprintf(p, "%d%%", percent); + } + return duration_format(p, ts->len); +} + +md_timeperiod_t md_timeperiod_slice_before_end(const md_timeperiod_t *period, + const md_timeslice_t *ts) +{ + md_timeperiod_t r; + apr_time_t duration = ts->len; + + if (ts->norm > 0) { + int percent = (int)(((long)apr_time_sec(ts->len)) * 100L + / ((long)apr_time_sec(ts->norm))); + apr_time_t plen = md_timeperiod_length(period); + if (apr_time_sec(plen) > 100) { + duration = apr_time_from_sec(apr_time_sec(plen) * percent / 100); + } + else { + duration = plen * percent / 100; + } + } + r.start = period->end - duration; + r.end = period->end; + return r; +} + +int md_timeslice_eq(const md_timeslice_t *ts1, const md_timeslice_t *ts2) +{ + if (ts1 == ts2) return 1; + if (!ts1 || !ts2) return 0; + return (ts1->norm == ts2->norm) && (ts1->len == ts2->len); +} + +md_timeperiod_t md_timeperiod_common(const md_timeperiod_t *a, const md_timeperiod_t *b) +{ + md_timeperiod_t c; + + c.start = (a->start > b->start)? a->start : b->start; + c.end = (a->end < b->end)? a->end : b->end; + if (c.start > c.end) { + c.start = c.end = 0; + } + return c; +} diff --git a/modules/md/md_time.h b/modules/md/md_time.h new file mode 100644 index 0000000..92bd9d8 --- /dev/null +++ b/modules/md/md_time.h @@ -0,0 +1,77 @@ +/* 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_time_h +#define mod_md_md_time_h + +#include <stdio.h> + +#define MD_SECS_PER_HOUR (60*60) +#define MD_SECS_PER_DAY (24*MD_SECS_PER_HOUR) + +typedef struct md_timeperiod_t md_timeperiod_t; + +struct md_timeperiod_t { + apr_time_t start; + apr_time_t end; +}; + +apr_time_t md_timeperiod_length(const md_timeperiod_t *period); + +int md_timeperiod_contains(const md_timeperiod_t *period, apr_time_t time); +int md_timeperiod_has_started(const md_timeperiod_t *period, apr_time_t time); +int md_timeperiod_has_ended(const md_timeperiod_t *period, apr_time_t time); +apr_interval_time_t md_timeperiod_remaining(const md_timeperiod_t *period, apr_time_t time); + +/** + * Return the timeperiod common between a and b. If both do not overlap, return {0,0}. + */ +md_timeperiod_t md_timeperiod_common(const md_timeperiod_t *a, const md_timeperiod_t *b); + +char *md_timeperiod_print(apr_pool_t *p, const md_timeperiod_t *period); + +/** + * Print a human readable form of the give duration in days/hours/min/sec + */ +const char *md_duration_print(apr_pool_t *p, apr_interval_time_t duration); +const char *md_duration_roughly(apr_pool_t *p, apr_interval_time_t duration); + +/** + * Parse a machine readable string duration in the form of NN[unit], where + * unit is d/h/mi/s/ms with the default given should the unit not be specified. + */ +apr_status_t md_duration_parse(apr_interval_time_t *ptimeout, const char *value, + const char *def_unit); +const char *md_duration_format(apr_pool_t *p, apr_interval_time_t duration); + +typedef struct { + apr_interval_time_t norm; /* if > 0, normalized base length */ + apr_interval_time_t len; /* length of the timespan */ +} md_timeslice_t; + +apr_status_t md_timeslice_create(md_timeslice_t **pts, apr_pool_t *p, + apr_interval_time_t norm, apr_interval_time_t len); + +int md_timeslice_eq(const md_timeslice_t *ts1, const md_timeslice_t *ts2); + +const char *md_timeslice_parse(md_timeslice_t **pts, apr_pool_t *p, + const char *val, apr_interval_time_t defnorm); +const char *md_timeslice_format(const md_timeslice_t *ts, apr_pool_t *p); + +md_timeperiod_t md_timeperiod_slice_before_end(const md_timeperiod_t *period, + const md_timeslice_t *ts); + +#endif /* md_util_h */ diff --git a/modules/md/md_util.c b/modules/md/md_util.c index 4e97d92..95ecc27 100644 --- a/modules/md/md_util.c +++ b/modules/md/md_util.c @@ -14,6 +14,7 @@ * limitations under the License. */ +#include <assert.h> #include <stdio.h> #include <apr_lib.h> @@ -24,6 +25,11 @@ #include <apr_tables.h> #include <apr_uri.h> +#if APR_HAVE_STDLIB_H +#include <stdlib.h> +#endif + +#include "md.h" #include "md_log.h" #include "md_util.h" @@ -35,8 +41,8 @@ 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) { + apr_pool_tag(ptemp, "md_pool_do"); rv = cb(baton, p, ptemp); - apr_pool_destroy(ptemp); } return rv; @@ -49,6 +55,7 @@ static apr_status_t pool_vado(md_util_vaction *cb, void *baton, apr_pool_t *p, v rv = apr_pool_create(&ptemp, p); if (APR_SUCCESS == rv) { + apr_pool_tag(ptemp, "md_pool_vado"); rv = cb(baton, p, ptemp, ap); apr_pool_destroy(ptemp); } @@ -67,8 +74,169 @@ apr_status_t md_util_pool_vdo(md_util_vaction *cb, void *baton, apr_pool_t *p, . } /**************************************************************************************************/ +/* data chunks */ + +void md_data_pinit(md_data_t *d, apr_size_t len, apr_pool_t *p) +{ + md_data_null(d); + d->data = apr_pcalloc(p, len); + d->len = len; +} + +md_data_t *md_data_pmake(apr_size_t len, apr_pool_t *p) +{ + md_data_t *d; + + d = apr_palloc(p, sizeof(*d)); + md_data_pinit(d, len, p); + return d; +} + +void md_data_init(md_data_t *d, const char *data, apr_size_t len) +{ + md_data_null(d); + d->len = len; + d->data = data; +} + +void md_data_init_str(md_data_t *d, const char *str) +{ + md_data_init(d, str, strlen(str)); +} + +void md_data_null(md_data_t *d) +{ + memset(d, 0, sizeof(*d)); +} + +void md_data_clear(md_data_t *d) +{ + if (d) { + if (d->data && d->free_data) d->free_data((void*)d->data); + memset(d, 0, sizeof(*d)); + } +} + +md_data_t *md_data_make_pcopy(apr_pool_t *p, const char *data, apr_size_t len) +{ + md_data_t *d; + + d = apr_palloc(p, sizeof(*d)); + d->len = len; + d->data = len? apr_pmemdup(p, data, len) : NULL; + return d; +} + +apr_status_t md_data_assign_copy(md_data_t *dest, const char *src, apr_size_t src_len) +{ + md_data_clear(dest); + if (src && src_len) { + dest->data = malloc(src_len); + if (!dest->data) return APR_ENOMEM; + memcpy((void*)dest->data, src, src_len); + dest->len = src_len; + dest->free_data = free; + } + return APR_SUCCESS; +} + +void md_data_assign_pcopy(md_data_t *dest, const char *src, apr_size_t src_len, apr_pool_t *p) +{ + md_data_clear(dest); + dest->data = (src && src_len)? apr_pmemdup(p, src, src_len) : NULL; + dest->len = dest->data? src_len : 0; +} + +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_data_to_hex(const char **phex, char separator, + apr_pool_t *p, const md_data_t *data) +{ + char *hex, *cp; + const char * x; + unsigned int i; + + cp = hex = apr_pcalloc(p, ((separator? 3 : 2) * data->len) + 1); + if (!hex) { + *phex = NULL; + return APR_ENOMEM; + } + for (i = 0; i < data->len; ++i) { + x = hex_const[(unsigned char)data->data[i]]; + if (i && separator) *cp++ = separator; + *cp++ = x[0]; + *cp++ = x[1]; + } + *phex = hex; + return APR_SUCCESS; +} + +/**************************************************************************************************/ +/* generic arrays */ + +int md_array_remove_at(struct apr_array_header_t *a, int idx) +{ + char *ps, *pe; + + if (idx < 0 || idx >= a->nelts) return 0; + if (idx+1 == a->nelts) { + --a->nelts; + } + else { + ps = (a->elts + (idx * a->elt_size)); + pe = ps + a->elt_size; + memmove(ps, pe, (size_t)((a->nelts - (idx+1)) * a->elt_size)); + --a->nelts; + } + return 1; +} + +int md_array_remove(struct apr_array_header_t *a, void *elem) +{ + int i, n, m; + void **pe; + + assert(sizeof(void*) == a->elt_size); + n = i = 0; + while (i < a->nelts) { + pe = &APR_ARRAY_IDX(a, i, void*); + if (*pe == elem) { + m = a->nelts - (i+1); + if (m > 0) memmove(pe, pe+1, (unsigned)m*sizeof(void*)); + a->nelts--; + n++; + continue; + } + ++i; + } + return n; +} + +/**************************************************************************************************/ /* string related */ +int md_array_is_empty(const struct apr_array_header_t *array) +{ + return (array == NULL) || (array->nelts == 0); +} + char *md_util_str_tolower(char *s) { char *orig = s; @@ -104,7 +272,7 @@ int md_array_str_eq(const struct apr_array_header_t *a1, const char *s1, *s2; if (a1 == a2) return 1; - if (!a1) return 0; + if (!a1 || !a2) return 0; if (a1->nelts != a2->nelts) return 0; for (i = 0; i < a1->nelts; ++i) { s1 = APR_ARRAY_IDX(a1, i, const char *); @@ -194,8 +362,20 @@ apr_status_t md_util_fopen(FILE **pf, const char *fn, const char *mode) 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 rv; + rv = apr_file_open(pf, fn, (APR_FOPEN_WRITE|APR_FOPEN_CREATE|APR_FOPEN_EXCL), + perms, p); + if (APR_SUCCESS == rv) { + /* See <https://github.com/icing/mod_md/issues/117> + * Some people set umask 007 to deny all world read/writability to files + * created by apache. While this is a noble effort, we need the store files + * to have the permissions as specified. */ + rv = apr_file_perms_set(fn, perms); + if (APR_STATUS_IS_ENOTIMPL(rv)) { + rv = APR_SUCCESS; + } + } + return rv; } apr_status_t md_util_is_dir(const char *path, apr_pool_t *pool) @@ -218,6 +398,21 @@ apr_status_t md_util_is_file(const char *path, apr_pool_t *pool) return rv; } +apr_status_t md_util_is_unix_socket(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_SOCK)? APR_SUCCESS : APR_EINVAL; + } + return rv; +} + +int md_file_exists(const char *fname, apr_pool_t *p) +{ + return (fname && *fname && APR_SUCCESS == md_util_is_file(fname, p)); +} + apr_status_t md_util_path_merge(const char **ppath, apr_pool_t *p, ...) { const char *segment, *path; @@ -248,7 +443,7 @@ apr_status_t md_util_freplace(const char *fpath, apr_fileperms_t perms, apr_pool creat: while (i < max && APR_EEXIST == (rv = md_util_fcreatex(&f, tmp, perms, p))) { ++i; - apr_sleep(apr_time_msec(50)); + apr_sleep(apr_time_from_msec(50)); } if (APR_EEXIST == rv && APR_SUCCESS == (rv = apr_file_remove(tmp, p)) @@ -312,6 +507,13 @@ apr_status_t md_text_fcreatex(const char *fpath, apr_fileperms_t perms, if (APR_SUCCESS == rv) { rv = write_text((void*)text, f, p); apr_file_close(f); + /* See <https://github.com/icing/mod_md/issues/117>: when a umask + * is set, files need to be assigned permissions explicitly. + * Otherwise, as in the issues reported, it will break our access model. */ + rv = apr_file_perms_set(fpath, perms); + if (APR_STATUS_IS_ENOTIMPL(rv)) { + rv = APR_SUCCESS; + } } return rv; } @@ -401,17 +603,25 @@ static apr_status_t match_and_do(md_util_fwalk_t *ctx, const char *path, int dep } pattern = APR_ARRAY_IDX(ctx->patterns, depth, const char *); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ptemp, "match_and_do " + "path=%s depth=%d pattern=%s", path, depth, pattern); rv = apr_dir_open(&d, path, ptemp); if (APR_SUCCESS != rv) { return rv; } while (APR_SUCCESS == (rv = apr_dir_read(&finfo, wanted, d))) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ptemp, "match_and_do " + "candidate=%s", finfo.name); if (!strcmp(".", finfo.name) || !strcmp("..", finfo.name)) { continue; } if (APR_SUCCESS == apr_fnmatch(pattern, finfo.name, 0)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ptemp, "match_and_do " + "candidate=%s matches pattern", finfo.name); if (ndepth < ctx->patterns->nelts) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ptemp, "match_and_do " + "need to go deeper"); 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); @@ -421,6 +631,8 @@ static apr_status_t match_and_do(md_util_fwalk_t *ctx, const char *path, int dep } } else { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, ptemp, "match_and_do " + "invoking inspector on name=%s", finfo.name); rv = ctx->cb(ctx->baton, p, ptemp, path, finfo.name, finfo.filetype); } } @@ -596,7 +808,7 @@ apr_status_t md_util_ftree_remove(const char *path, apr_pool_t *p) /* DNS name checks ********************************************************************************/ -int md_util_is_dns_name(apr_pool_t *p, const char *hostname, int need_fqdn) +int md_dns_is_name(apr_pool_t *p, const char *hostname, int need_fqdn) { char c, last = 0; const char *cp = hostname; @@ -637,6 +849,86 @@ int md_util_is_dns_name(apr_pool_t *p, const char *hostname, int need_fqdn) return 1; /* empty string not allowed */ } +int md_dns_is_wildcard(apr_pool_t *p, const char *domain) +{ + if (domain[0] != '*' || domain[1] != '.') return 0; + return md_dns_is_name(p, domain+2, 1); +} + +int md_dns_matches(const char *pattern, const char *domain) +{ + const char *s; + + if (!apr_strnatcasecmp(pattern, domain)) return 1; + if (pattern[0] == '*' && pattern[1] == '.') { + s = strchr(domain, '.'); + if (s && !apr_strnatcasecmp(pattern+1, s)) return 1; + } + return 0; +} + +apr_array_header_t *md_dns_make_minimal(apr_pool_t *p, apr_array_header_t *domains) +{ + apr_array_header_t *minimal; + const char *domain, *pattern; + int i, j, duplicate; + + minimal = apr_array_make(p, domains->nelts, sizeof(const char *)); + for (i = 0; i < domains->nelts; ++i) { + domain = APR_ARRAY_IDX(domains, i, const char*); + duplicate = 0; + /* is it matched in minimal already? */ + for (j = 0; j < minimal->nelts; ++j) { + pattern = APR_ARRAY_IDX(minimal, j, const char*); + if (md_dns_matches(pattern, domain)) { + duplicate = 1; + break; + } + } + if (!duplicate) { + if (!md_dns_is_wildcard(p, domain)) { + /* plain name, will we see a wildcard that replaces it? */ + for (j = i+1; j < domains->nelts; ++j) { + pattern = APR_ARRAY_IDX(domains, j, const char*); + if (md_dns_is_wildcard(p, pattern) && md_dns_matches(pattern, domain)) { + duplicate = 1; + break; + } + } + } + if (!duplicate) { + APR_ARRAY_PUSH(minimal, const char *) = domain; + } + } + } + return minimal; +} + +int md_dns_domains_match(const apr_array_header_t *domains, const char *name) +{ + const char *domain; + int i; + + for (i = 0; i < domains->nelts; ++i) { + domain = APR_ARRAY_IDX(domains, i, const char*); + if (md_dns_matches(domain, name)) return 1; + } + return 0; +} + +int md_is_wild_match(const apr_array_header_t *domains, const char *name) +{ + const char *domain; + int i; + + for (i = 0; i < domains->nelts; ++i) { + domain = APR_ARRAY_IDX(domains, i, const char*); + if (md_dns_matches(domain, name)) + return (domain[0] == '*' && domain[1] == '.'); + } + return 0; +} + const char *md_util_schemify(apr_pool_t *p, const char *s, const char *def_scheme) { const char *cp = s; @@ -670,7 +962,7 @@ static apr_status_t uri_check(apr_uri_t *uri_parsed, apr_pool_t *p, if (!uri_parsed->hostname) { err = "missing hostname"; } - else if (!md_util_is_dns_name(p, uri_parsed->hostname, 0)) { + else if (!md_dns_is_name(p, uri_parsed->hostname, 0)) { err = "invalid hostname"; } if (uri_parsed->port_str @@ -789,45 +1081,44 @@ apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs, /* 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 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; - + char buffer[1024]; + *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; + APR_NO_PIPE, APR_FULL_BLOCK)) + && APR_SUCCESS == (rv = apr_procattr_cmdtype_set(procattr, APR_PROGRAM_ENV)) + && APR_SUCCESS == (rv = apr_proc_create(proc, cmd, argv, NULL, procattr, p))) { + + /* read stderr and log on INFO for possible fault analysis. */ + while(APR_SUCCESS == (rv = apr_file_gets(buffer, sizeof(buffer)-1, proc->err))) { + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, "cmd(%s) stderr: %s", cmd, buffer); + } + if (!APR_STATUS_IS_EOF(rv)) goto out; + apr_file_close(proc->err); + + if (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 APR_SUCCESS; } +out: 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 @@ -863,7 +1154,7 @@ static const unsigned char BASE64URL_CHARS[] = { #define BASE64URL_CHAR(x) BASE64URL_CHARS[ (unsigned int)(x) & 0x3fu ] -apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, +apr_size_t md_util_base64url_decode(md_data_t *decoded, const char *encoded, apr_pool_t *pool) { const unsigned char *e = (const unsigned char *)encoded; @@ -877,10 +1168,10 @@ apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, } len = (int)(p - e); mlen = (len/4)*4; - *decoded = apr_pcalloc(pool, (apr_size_t)len + 1); + decoded->data = apr_pcalloc(pool, (apr_size_t)len + 1); i = 0; - d = (unsigned char*)*decoded; + d = (unsigned char*)decoded->data; for (; i < mlen; i += 4) { n = ((BASE64URL_UINT6[ e[i+0] ] << 18) + (BASE64URL_UINT6[ e[i+1] ] << 12) + @@ -909,14 +1200,15 @@ apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, default: /* do nothing */ break; } - return (apr_size_t)(mlen/4*3 + remain); + decoded->len = (apr_size_t)(mlen/4*3 + remain); + return decoded->len; } -const char *md_util_base64url_encode(const char *data, apr_size_t dlen, apr_pool_t *pool) +const char *md_util_base64url_encode(const md_data_t *data, 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; + int i, len = (int)data->len; + apr_size_t slen = ((data->len+2)/3)*4 + 1; /* 0 terminated */ + const unsigned char *udata = (const unsigned char*)data->data; unsigned char *enc, *p = apr_pcalloc(pool, slen); enc = p; @@ -1252,3 +1544,23 @@ const char *md_link_find_relation(const apr_table_t *headers, return ctx.url; } +const char *md_util_parse_ct(apr_pool_t *pool, const char *cth) +{ + char *type; + const char *p; + apr_size_t hlen; + + if (!cth) return NULL; + + for( p = cth; *p && *p != ' ' && *p != ';'; ++p) + ; + hlen = (apr_size_t)(p - cth); + type = apr_pcalloc( pool, hlen + 1 ); + assert(type); + memcpy(type, cth, hlen); + type[hlen] = '\0'; + + return type; + /* Could parse and return parameters here, but we don't need any at present. + */ +} diff --git a/modules/md/md_util.h b/modules/md/md_util.h index 5b3a2ea..d974788 100644 --- a/modules/md/md_util.h +++ b/modules/md/md_util.h @@ -33,9 +33,79 @@ 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, ...); /**************************************************************************************************/ +/* data chunks */ + +typedef void md_data_free_fn(void *data); + +typedef struct md_data_t md_data_t; +struct md_data_t { + const char *data; + apr_size_t len; + md_data_free_fn *free_data; +}; + +/** + * Init the data to empty, overwriting any content. + */ +void md_data_null(md_data_t *d); + +/** + * Create a new md_data_t, providing `len` bytes allocated from pool `p`. + */ +md_data_t *md_data_pmake(apr_size_t len, apr_pool_t *p); +/** + * Initialize md_data_t 'd', providing `len` bytes allocated from pool `p`. + */ +void md_data_pinit(md_data_t *d, apr_size_t len, apr_pool_t *p); +/** + * Initialize md_data_t 'd', by borrowing 'len' bytes in `data` without copying. + * `d` will not take ownership. + */ +void md_data_init(md_data_t *d, const char *data, apr_size_t len); + +/** + * Initialize md_data_t 'd', by borrowing the NUL-terminated `str`. + * `d` will not take ownership. + */ +void md_data_init_str(md_data_t *d, const char *str); + +/** + * Free any present data and clear (NULL) it. Passing NULL is permitted. + */ +void md_data_clear(md_data_t *d); + +md_data_t *md_data_make_pcopy(apr_pool_t *p, const char *data, apr_size_t len); + +apr_status_t md_data_assign_copy(md_data_t *dest, const char *src, apr_size_t src_len); +void md_data_assign_pcopy(md_data_t *dest, const char *src, apr_size_t src_len, apr_pool_t *p); + +apr_status_t md_data_to_hex(const char **phex, char separator, + apr_pool_t *p, const md_data_t *data); + +/**************************************************************************************************/ +/* generic arrays */ + +/** + * In an array of pointers, remove all entries == elem. Returns the number + * of entries removed. + */ +int md_array_remove(struct apr_array_header_t *a, void *elem); + +/* + * Remove the ith entry from the array. + * @return != 0 iff an entry was removed, e.g. idx was not outside range + */ +int md_array_remove_at(struct apr_array_header_t *a, int idx); + +/**************************************************************************************************/ /* string related */ char *md_util_str_tolower(char *s); +/** + * Return != 0 iff array is either NULL or empty + */ +int md_array_is_empty(const struct apr_array_header_t *array); + int md_array_str_index(const struct apr_array_header_t *array, const char *s, int start, int case_sensitive); @@ -44,9 +114,15 @@ int md_array_str_eq(const struct apr_array_header_t *a1, struct apr_array_header_t *md_array_str_clone(apr_pool_t *p, struct apr_array_header_t *array); +/** + * Create a new array with duplicates removed. + */ struct apr_array_header_t *md_array_str_compact(apr_pool_t *p, struct apr_array_header_t *src, int case_sensitive); +/** + * Create a new array with all occurrences of <exclude> removed. + */ 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); @@ -55,13 +131,53 @@ int md_array_str_add_missing(struct apr_array_header_t *dest, /**************************************************************************************************/ /* 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); +/** + * Is a host/domain name using allowed characters. Not a wildcard. + * @param domain name to check + * @param need_fqdn iff != 0, check that domain contains '.' + * @return != 0 iff domain looks like a non-wildcard, legal DNS domain name. + */ +int md_dns_is_name(apr_pool_t *p, const char *domain, int need_fqdn); + +/** + * Check if the given domain is a valid wildcard DNS name, e.g. *.example.org + * @param domain name to check + * @return != 0 iff domain is a DNS wildcard. + */ +int md_dns_is_wildcard(apr_pool_t *p, const char *domain); + +/** + * Determine iff pattern matches domain, including case-ignore and wildcard domains. + * It is assumed that both names follow dns syntax. + * @return != 0 iff pattern matches domain + */ +int md_dns_matches(const char *pattern, const char *domain); + +/** + * Create a new array with the minimal set out of the given domain names that match all + * of them. If none of the domains is a wildcard, only duplicates are removed. + * If domains contain a wildcard, any name matching the wildcard will be removed. + */ +struct apr_array_header_t *md_dns_make_minimal(apr_pool_t *p, + struct apr_array_header_t *domains); + +/** + * Determine if the given domains cover the name, including wildcard matching. + * @return != 0 iff name is matched by list of domains + */ +int md_dns_domains_match(const apr_array_header_t *domains, const char *name); + +/** + * @return != 0 iff `name` is matched by a wildcard pattern in `domains` + */ +int md_is_wild_match(const apr_array_header_t *domains, const char *name); /**************************************************************************************************/ /* file system related */ @@ -78,6 +194,8 @@ 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); +apr_status_t md_util_is_unix_socket(const char *path, apr_pool_t *pool); +int md_file_exists(const char *fname, apr_pool_t *p); typedef apr_status_t md_util_file_cb(void *baton, struct apr_file_t *f, apr_pool_t *p); @@ -113,9 +231,8 @@ apr_status_t md_text_freplace(const char *fpath, apr_fileperms_t perms, /**************************************************************************************************/ /* 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, +const char *md_util_base64url_encode(const md_data_t *data, apr_pool_t *pool); +apr_size_t md_util_base64url_decode(md_data_t *decoded, const char *encoded, apr_pool_t *pool); /**************************************************************************************************/ @@ -128,6 +245,7 @@ apr_status_t md_util_abs_http_uri_check(apr_pool_t *p, const char *uri, const ch const char *md_link_find_relation(const struct apr_table_t *headers, apr_pool_t *pool, const char *relation); +const char *md_util_parse_ct(apr_pool_t *pool, const char *cth); /**************************************************************************************************/ /* retry logic */ @@ -137,12 +255,4 @@ 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 index 48e91a0..86a1821 100644 --- a/modules/md/md_version.h +++ b/modules/md/md_version.h @@ -27,7 +27,7 @@ * @macro * Version number of the md module as c string */ -#define MOD_MD_VERSION "1.1.17" +#define MOD_MD_VERSION "2.4.25" /** * @macro @@ -35,8 +35,9 @@ * 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 MOD_MD_VERSION_NUM 0x020419 -#define MD_ACME_DEF_URL "https://acme-v01.api.letsencrypt.org/directory" +#define MD_ACME_DEF_URL "https://acme-v02.api.letsencrypt.org/directory" +#define MD_TAILSCALE_DEF_URL "file://localhost/var/run/tailscale/tailscaled.sock" #endif /* mod_md_md_version_h */ diff --git a/modules/md/mod_md.c b/modules/md/mod_md.c index 249a0f0..6d3f5b7 100644 --- a/modules/md/mod_md.c +++ b/modules/md/mod_md.c @@ -13,33 +13,36 @@ * 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_ssl.h> #include <http_log.h> #include <http_vhost.h> #include <ap_listen.h> +#include "mod_status.h" + #include "md.h" #include "md_curl.h" #include "md_crypt.h" +#include "md_event.h" #include "md_http.h" #include "md_json.h" #include "md_store.h" #include "md_store_fs.h" #include "md_log.h" +#include "md_ocsp.h" +#include "md_result.h" #include "md_reg.h" +#include "md_status.h" #include "md_util.h" #include "md_version.h" #include "md_acme.h" @@ -47,9 +50,10 @@ #include "mod_md.h" #include "mod_md_config.h" +#include "mod_md_drive.h" +#include "mod_md_ocsp.h" #include "mod_md_os.h" -#include "mod_ssl.h" -#include "mod_watchdog.h" +#include "mod_md_status.h" static void md_hooks(apr_pool_t *pool); @@ -66,14 +70,251 @@ AP_DECLARE_MODULE(md) = { #endif }; -static void md_merge_srv(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p) +/**************************************************************************************************/ +/* 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, (int)level, rv, log_server, "%s", buffer); + } + else { + ap_log_perror(file, line, APLOG_MODULE_INDEX, (int)level, rv, p, "%s", buffer); + } + } +} + +/**************************************************************************************************/ +/* mod_ssl interface */ + +static void init_ssl(void) +{ + /* nop */ +} + +/**************************************************************************************************/ +/* 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); +} + +/**************************************************************************************************/ +/* notification handling */ + +typedef struct { + const char *reason; /* what the notification is about */ + apr_time_t min_interim; /* minimum time between notifying for this reason */ +} notify_rate; + +static notify_rate notify_rates[] = { + { "renewing", apr_time_from_sec(MD_SECS_PER_HOUR) }, /* once per hour */ + { "renewed", apr_time_from_sec(MD_SECS_PER_DAY) }, /* once per day */ + { "installed", apr_time_from_sec(MD_SECS_PER_DAY) }, /* once per day */ + { "expiring", apr_time_from_sec(MD_SECS_PER_DAY) }, /* once per day */ + { "errored", apr_time_from_sec(MD_SECS_PER_HOUR) }, /* once per hour */ + { "ocsp-renewed", apr_time_from_sec(MD_SECS_PER_DAY) }, /* once per day */ + { "ocsp-errored", apr_time_from_sec(MD_SECS_PER_HOUR) }, /* once per hour */ +}; + +static apr_status_t notify(md_job_t *job, const char *reason, + md_result_t *result, apr_pool_t *p, void *baton) +{ + md_mod_conf_t *mc = baton; + const char * const *argv; + const char *cmdline; + int exit_code; + apr_status_t rv = APR_SUCCESS; + apr_time_t min_interim = 0; + md_timeperiod_t since_last; + const char *log_msg_reason; + int i; + + log_msg_reason = apr_psprintf(p, "message-%s", reason); + for (i = 0; i < (int)(sizeof(notify_rates)/sizeof(notify_rates[0])); ++i) { + if (!strcmp(reason, notify_rates[i].reason)) { + min_interim = notify_rates[i].min_interim; + } + } + if (min_interim > 0) { + since_last.start = md_job_log_get_time_of_latest(job, log_msg_reason); + since_last.end = apr_time_now(); + if (since_last.start > 0 && md_timeperiod_length(&since_last) < min_interim) { + /* not enough time has passed since we sent the last notification + * for this reason. */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, APLOGNO(10267) + "%s: rate limiting notification about '%s'", job->mdomain, reason); + return APR_SUCCESS; + } + } + + if (!strcmp("renewed", reason)) { + if (mc->notify_cmd) { + cmdline = apr_psprintf(p, "%s %s", mc->notify_cmd, job->mdomain); + apr_tokenize_to_argv(cmdline, (char***)&argv, p); + rv = md_util_exec(p, argv[0], argv, &exit_code); + + if (APR_SUCCESS == rv && exit_code) rv = APR_EGENERAL; + if (APR_SUCCESS != rv) { + md_result_problem_printf(result, rv, MD_RESULT_LOG_ID(APLOGNO(10108)), + "MDNotifyCmd %s failed with exit code %d.", + mc->notify_cmd, exit_code); + md_result_log(result, MD_LOG_ERR); + md_job_log_append(job, "notify-error", result->problem, result->detail); + return rv; + } + } + md_log_perror(MD_LOG_MARK, MD_LOG_NOTICE, 0, p, APLOGNO(10059) + "The Managed Domain %s has been setup and changes " + "will be activated on next (graceful) server restart.", job->mdomain); + } + if (mc->message_cmd) { + cmdline = apr_psprintf(p, "%s %s %s", mc->message_cmd, reason, job->mdomain); + apr_tokenize_to_argv(cmdline, (char***)&argv, p); + rv = md_util_exec(p, argv[0], argv, &exit_code); + + if (APR_SUCCESS == rv && exit_code) rv = APR_EGENERAL; + if (APR_SUCCESS != rv) { + md_result_problem_printf(result, rv, MD_RESULT_LOG_ID(APLOGNO(10109)), + "MDMessageCmd %s failed with exit code %d.", + mc->message_cmd, exit_code); + md_result_log(result, MD_LOG_ERR); + md_job_log_append(job, "message-error", reason, result->detail); + return rv; + } + } + + md_job_log_append(job, log_msg_reason, NULL, NULL); + return APR_SUCCESS; +} + +static apr_status_t on_event(const char *event, const char *mdomain, void *baton, + md_job_t *job, md_result_t *result, apr_pool_t *p) +{ + (void)mdomain; + return notify(job, event, result, p, baton); +} + +/**************************************************************************************************/ +/* store setup */ + +static apr_status_t store_file_ev(void *baton, struct md_store_t *store, + md_store_fs_ev_t ev, unsigned 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, STAGING and OCSP are written to + * under a different user. Give her ownership. + */ + if (ftype == APR_DIR) { + switch (group) { + case MD_SG_CHALLENGES: + case MD_SG_STAGING: + case MD_SG_OCSP: + 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; + + base_dir = ap_server_root_relative(p, mc->base_dir); + + if (APR_SUCCESS != (rv = 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 leave; + } + + md_store_fs_set_event_cb(*pstore, store_file_ev, s); + if (APR_SUCCESS != (rv = check_group_dir(*pstore, MD_SG_CHALLENGES, p, s)) + || APR_SUCCESS != (rv = check_group_dir(*pstore, MD_SG_STAGING, p, s)) + || APR_SUCCESS != (rv = check_group_dir(*pstore, MD_SG_ACCOUNTS, p, s)) + || APR_SUCCESS != (rv = check_group_dir(*pstore, MD_SG_OCSP, p, s)) + ) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10047) + "setup challenges directory"); + goto leave; + } + +leave: + return rv; +} + +/**************************************************************************************************/ +/* post config handling */ + +static void merge_srv_config(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p) +{ + const char *contact; + 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_urls && md->sc->ca_urls) { + md->ca_urls = apr_array_copy(p, md->sc->ca_urls); } if (!md->ca_proto) { md->ca_proto = md_config_gets(md->sc, MD_CONFIG_CA_PROTO); @@ -81,90 +322,92 @@ static void md_merge_srv(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p) 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)) { + contact = md_config_gets(md->sc, MD_CONFIG_CA_CONTACT); + if (md->contacts && md->contacts->nelts > 0) { + /* set explicitly */ + } + else if (contact && contact[0]) { apr_array_clear(md->contacts); - APR_ARRAY_PUSH(md->contacts, const char *) = - md_util_schemify(p, md->sc->s->server_admin, "mailto"); + APR_ARRAY_PUSH(md->contacts, const char *) = + md_util_schemify(p, contact, "mailto"); } - if (md->drive_mode == MD_DRIVE_DEFAULT) { - md->drive_mode = md_config_geti(md->sc, MD_CONFIG_DRIVE_MODE); + else 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->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->renew_mode == MD_RENEW_DEFAULT) { + md->renew_mode = md_config_geti(md->sc, MD_CONFIG_DRIVE_MODE); } + if (!md->renew_window) md_config_get_timespan(&md->renew_window, md->sc, MD_CONFIG_RENEW_WINDOW); + if (!md->warn_window) md_config_get_timespan(&md->warn_window, md->sc, MD_CONFIG_WARN_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_pkeys_spec_is_empty(md->pks)) { + md->pks = md->sc->pks; } if (md->require_https < 0) { md->require_https = md_config_geti(md->sc, MD_CONFIG_REQUIRE_HTTPS); } + if (!md->ca_eab_kid) { + md->ca_eab_kid = md->sc->ca_eab_kid; + md->ca_eab_hmac = md->sc->ca_eab_hmac; + } if (md->must_staple < 0) { md->must_staple = md_config_geti(md->sc, MD_CONFIG_MUST_STAPLE); } + if (md->stapling < 0) { + md->stapling = md_config_geti(md->sc, MD_CONFIG_STAPLING); + } } -static apr_status_t check_coverage(md_t *md, const char *domain, server_rec *s, apr_pool_t *p) +static apr_status_t check_coverage(md_t *md, const char *domain, server_rec *s, + int *pupdates, 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); + *pupdates |= MD_UPD_DOMAINS; return APR_SUCCESS; } else { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(10040) + ap_log_error(APLOG_MARK, APLOG_WARNING, 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; + return APR_SUCCESS; } } -static apr_status_t md_covers_server(md_t *md, server_rec *s, apr_pool_t *p) +static apr_status_t md_cover_server(md_t *md, server_rec *s, int *pupdates, 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) { + + if (APR_SUCCESS == (rv = check_coverage(md, s->server_hostname, s, pupdates, p))) { + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, + "md[%s]: auto add, covers name %s", md->name, s->server_hostname); + for (i = 0; s->names && i < s->names->nelts; ++i) { name = APR_ARRAY_IDX(s->names, i, const char*); - if (APR_SUCCESS != (rv = check_coverage(md, name, s, p))) { + if (APR_SUCCESS != (rv = check_coverage(md, name, s, pupdates, p))) { break; } + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, + "md[%s]: auto add, covers alias %s", md->name, name); } } 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) +static int uses_port(server_rec *s, int port) { server_addr_rec *sa; int match = 0; @@ -181,839 +424,444 @@ static int uses_port_only(server_rec *s, int port) return match; } -static apr_status_t assign_to_servers(md_t *md, server_rec *base_server, - apr_pool_t *p, apr_pool_t *ptemp) +static apr_status_t detect_supported_protocols(md_mod_conf_t *mc, server_rec *s, + apr_pool_t *p, int log_level) +{ + ap_listen_rec *lr; + apr_sockaddr_t *sa; + int can_http, can_https; + + if (mc->can_http >= 0 && mc->can_https >= 0) goto set_and_leave; + + can_http = 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))) { + can_http = 1; + } + else if (sa->port == mc->local_443 + && (!lr->protocol || !strncmp("http", lr->protocol, 4))) { + can_https = 1; + } + } + } + if (mc->can_http < 0) mc->can_http = can_http; + if (mc->can_https < 0) mc->can_https = can_https; + ap_log_error(APLOG_MARK, log_level, 0, s, APLOGNO(10037) + "server seems%s reachable via http: and%s reachable via https:", + mc->can_http? "" : " not", mc->can_https? "" : " not"); +set_and_leave: + return md_reg_set_props(mc->reg, p, mc->can_http, mc->can_https); +} + +static server_rec *get_public_https_server(md_t *md, const char *domain, server_rec *base_server) { - server_rec *s, *s_https; - request_rec r; md_srv_conf_t *sc; md_mod_conf_t *mc; + server_rec *s; + server_rec *res = NULL; + request_rec r; + int i; + int check_port = 1; + + sc = md_config_get(base_server); + mc = sc->mc; + memset(&r, 0, sizeof(r)); + + if (md->ca_challenges && md->ca_challenges->nelts > 0) { + /* skip the port check if "tls-alpn-01" is pre-configured */ + check_port = !(md_array_str_index(md->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0, 0) >= 0); + } + + if (check_port && !mc->can_https) return NULL; + + /* find an ssl server matching domain from MD */ + for (s = base_server; s; s = s->next) { + sc = md_config_get(s); + if (!sc || !sc->is_ssl || !sc->assigned) continue; + if (base_server == s && !mc->manage_base_server) continue; + if (base_server != s && check_port && mc->local_443 > 0 && !uses_port(s, mc->local_443)) continue; + for (i = 0; i < sc->assigned->nelts; ++i) { + if (md == APR_ARRAY_IDX(sc->assigned, i, md_t*)) { + r.server = s; + if (ap_matches_request_vhost(&r, domain, s->port)) { + if (check_port) { + return s; + } + else { + /* there may be multiple matching servers because we ignore the port. + if possible, choose a server that supports the acme-tls/1 protocol */ + if (ap_is_allowed_protocol(NULL, NULL, s, PROTO_ACME_TLS_1)) { + return s; + } + res = s; + } + } + } + } + } + return res; +} + +static apr_status_t auto_add_domains(md_t *md, server_rec *base_server, apr_pool_t *p) +{ + md_srv_conf_t *sc; + server_rec *s; apr_status_t rv = APR_SUCCESS; + int updates; + + /* Ad all domain names used in SSL VirtualHosts, if not already there */ + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, base_server, + "md[%s]: auto add domains", md->name); + updates = 0; + for (s = base_server; s; s = s->next) { + sc = md_config_get(s); + if (!sc || !sc->is_ssl || !sc->assigned || sc->assigned->nelts != 1) continue; + if (md != APR_ARRAY_IDX(sc->assigned, 0, md_t*)) continue; + if (APR_SUCCESS != (rv = md_cover_server(md, s, &updates, p))) { + return rv; + } + } + return rv; +} + +static void init_acme_tls_1_domains(md_t *md, server_rec *base_server) +{ + md_srv_conf_t *sc; + md_mod_conf_t *mc; + server_rec *s; int i; const char *domain; - apr_array_header_t *servers; - + + /* Collect those domains that support the "acme-tls/1" protocol. This + * is part of the MD (and not tested dynamically), since challenge selection + * may be done outside the server, e.g. in the a2md command. */ sc = md_config_get(base_server); mc = sc->mc; + apr_array_clear(md->acme_tls_1_domains); + for (i = 0; i < md->domains->nelts; ++i) { + domain = APR_ARRAY_IDX(md->domains, i, const char*); + s = get_public_https_server(md, domain, base_server); + /* If we did not find a specific virtualhost for md and manage + * the base_server, that one is inspected */ + if (NULL == s && mc->manage_base_server) s = base_server; + if (NULL == s) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10168) + "%s: no https server_rec found for %s", md->name, domain); + continue; + } + if (!ap_is_allowed_protocol(NULL, NULL, s, PROTO_ACME_TLS_1)) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10169) + "%s: https server_rec for %s does not have protocol %s enabled", + md->name, domain, PROTO_ACME_TLS_1); + continue; + } + APR_ARRAY_PUSH(md->acme_tls_1_domains, const char*) = domain; + } +} + +static apr_status_t link_md_to_servers(md_mod_conf_t *mc, md_t *md, server_rec *base_server, + apr_pool_t *p) +{ + server_rec *s; + request_rec r; + md_srv_conf_t *sc; + int i; + const char *domain, *uri; + + sc = md_config_get(base_server); /* 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)) { + + if ((mc->match_mode == MD_MATCH_ALL && + ap_matches_request_vhost(&r, domain, s->port)) + || (((mc->match_mode == MD_MATCH_SERVERNAMES) || md_dns_is_wildcard(p, domain)) && + md_dns_matches(domain, s->server_hostname))) { /* 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; + if (!sc->assigned) sc->assigned = apr_array_make(p, 2, sizeof(md_t*)); + if (sc->assigned->nelts == 1 && mc->match_mode == MD_MATCH_SERVERNAMES) { + /* there is already an MD assigned for this server. But in + * this match mode, wildcard matches are pre-empted by non-wildcards */ + int existing_wild = md_is_wild_match( + APR_ARRAY_IDX(sc->assigned, 0, const md_t*)->domains, + s->server_hostname); + if (!existing_wild && md_dns_is_wildcard(p, domain)) + continue; /* do not add */ + if (existing_wild && !md_dns_is_wildcard(p, domain)) + sc->assigned->nelts = 0; /* overwrite existing */ } + APR_ARRAY_PUSH(sc->assigned, md_t*) = md; + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10041) + "Server %s:%d matches md %s (config %s, match-mode=%d) " + "for domain %s, has now %d MDs", + s->server_hostname, s->port, md->name, sc->name, + mc->match_mode, domain, (int)sc->assigned->nelts); - 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->contacts && md->contacts->nelts > 0) { + /* set explicitly */ + } + else if (sc->ca_contact && sc->ca_contact[0]) { + uri = md_util_schemify(p, sc->ca_contact, "mailto"); if (md_array_str_index(md->contacts, uri, 0, 0) < 0) { - APR_ARRAY_PUSH(md->contacts, const char *) = uri; + 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; + else 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(10237) + "%s: added contact %s", md->name, uri); } } - - 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); - } + break; } } - } + return APR_SUCCESS; +} + +static apr_status_t link_mds_to_servers(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) +{ + int i; + md_t *md; + apr_status_t rv = APR_SUCCESS; + + apr_array_clear(mc->unused_names); + for (i = 0; i < mc->mds->nelts; ++i) { + md = APR_ARRAY_IDX(mc->mds, i, md_t*); + if (APR_SUCCESS != (rv = link_md_to_servers(mc, md, s, p))) { + goto leave; + } + } +leave: 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) +static apr_status_t merge_mds_with_conf(md_mod_conf_t *mc, apr_pool_t *p, + server_rec *base_server, int log_level) { - md_srv_conf_t *sc; - md_mod_conf_t *mc; + md_srv_conf_t *base_conf; md_t *md, *omd; const char *domain; + md_timeslice_t *ts; 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; + /* The global module configuration 'mc' keeps a list of all configured MDomains + * in the server. This list is collected during configuration processing and, + * in the post config phase, get updated from all merged server configurations + * before the server starts processing. + */ + base_conf = md_config_get(base_server); + md_config_get_timespan(&ts, base_conf, MD_CONFIG_RENEW_WINDOW); + if (ts) md_reg_set_renew_window_default(mc->reg, ts); + md_config_get_timespan(&ts, base_conf, MD_CONFIG_WARN_WINDOW); + if (ts) md_reg_set_warn_window_default(mc->reg, ts); - 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. + * 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); + merge_srv_config(md, base_conf, p); + + if (mc->match_mode == MD_MATCH_ALL) { + /* 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; + } + } + } + + if (md->cert_files && md->cert_files->nelts) { + if (!md->pkey_files || (md->cert_files->nelts != md->pkey_files->nelts)) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10170) + "The Managed Domain '%s' " + "needs one MDCertificateKeyFile for each MDCertificateFile.", + md->name); 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; + else if (md->pkey_files && md->pkey_files->nelts + && (!md->cert_files || !md->cert_files->nelts)) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10171) + "The Managed Domain '%s' " + "has MDCertificateKeyFile(s) but no MDCertificateFile.", + md->name); + return APR_EINVAL; } - 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; + if (APLOG_IS_LEVEL(base_server, log_level)) { + ap_log_error(APLOG_MARK, log_level, 0, base_server, APLOGNO(10039) + "Completed MD[%s, CA=%s, Proto=%s, Agreement=%s, renew-mode=%d " + "renew_window=%s, warn_window=%s", + md->name, md->ca_effective, md->ca_proto, md->ca_agreement, md->renew_mode, + md->renew_window? md_timeslice_format(md->renew_window, p) : "unset", + md->warn_window? md_timeslice_format(md->warn_window, p) : "unset"); } } - 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) +static apr_status_t check_invalid_duplicates(server_rec *base_server) { + server_rec *s; 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); + ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, base_server, + "checking duplicate ssl assignments"); + for (s = base_server; s; s = s->next) { + sc = md_config_get(s); + if (!sc || !sc->assigned) continue; + + if (sc->assigned->nelts > 1 && sc->is_ssl) { + /* duplicate assignment to SSL VirtualHost, not allowed */ + ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10042) + "conflict: %d MDs match to SSL VirtualHost %s, there can at most be one.", + (int)sc->assigned->nelts, s->server_hostname); + return APR_EINVAL; } } -} - -/**************************************************************************************************/ -/* 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) +static apr_status_t check_usage(md_mod_conf_t *mc, md_t *md, server_rec *base_server, + apr_pool_t *p, apr_pool_t *ptemp) { - 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) -{ + md_srv_conf_t *sc; 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); + int i, has_ssl; + apr_array_header_t *servers; + + (void)p; + servers = apr_array_make(ptemp, 5, sizeof(server_rec*)); + has_ssl = 0; + for (s = base_server; s; s = s->next) { + sc = md_config_get(s); + if (!sc || !sc->assigned) continue; + for (i = 0; i < sc->assigned->nelts; ++i) { + if (md == APR_ARRAY_IDX(sc->assigned, i, md_t*)) { + APR_ARRAY_PUSH(servers, server_rec*) = s; + if (sc->is_ssl) has_ssl = 1; } } - 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; + + if (!has_ssl && md->require_https > MD_REQUIRE_OFF) { + /* We require https for this MD, but do we have a SSL vhost? */ + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, base_server, APLOGNO(10105) + "MD %s does not match any VirtualHost with 'SSLEngine on', " + "but is configured to require https. This cannot work.", md->name); } - 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); + if (apr_is_empty_array(servers)) { + if (md->renew_mode != MD_RENEW_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; } - 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) +static int init_cert_watch_status(md_mod_conf_t *mc, apr_pool_t *p, apr_pool_t *ptemp, server_rec *s) { - 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; - } - } + md_t *md; + md_result_t *result; + int i, count; - 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; - } + /* Calculate the list of MD names which we need to watch: + * - all MDs that are used somewhere + * - all MDs in drive mode 'AUTO' that are not in 'unused_names' + */ + count = 0; + result = md_result_make(ptemp, APR_SUCCESS); + for (i = 0; i < mc->mds->nelts; ++i) { + md = APR_ARRAY_IDX(mc->mds, i, md_t*); + md_result_set(result, APR_SUCCESS, NULL); + md->watched = 0; + if (md->state == MD_S_ERROR) { + md_result_set(result, APR_EGENERAL, + "in error state, unable to drive forward. This " + "indicates an incomplete or inconsistent configuration. " + "Please check the log for warnings in this regard."); + continue; } - 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); + if (md->renew_mode == MD_RENEW_AUTO + && md_array_str_index(mc->unused_names, md->name, 0, 0) >= 0) { + /* This MD is not used in any virtualhost, do not watch */ + continue; } - } - - 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 (md_will_renew_cert(md)) { + /* make a test init to detect early errors. */ + md_reg_test_init(mc->reg, md, mc->env, result, p); + if (APR_SUCCESS != result->status && result->detail) { + apr_hash_set(mc->init_errors, md->name, APR_HASH_KEY_STRING, apr_pstrdup(p, result->detail)); + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(10173) + "md[%s]: %s", md->name, result->detail); } } - } - 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; + md->watched = 1; + ++count; } - 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; + return count; } -static apr_status_t md_post_config(apr_pool_t *p, apr_pool_t *plog, - apr_pool_t *ptemp, server_rec *s) +static apr_status_t md_post_config_before_ssl(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; + int dry_run = 0, log_level = APLOG_DEBUG; + md_store_t *store; 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. + * need to do to find out if the settings are ok. More expensive + * things we delay to the real run. */ - ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10070) + dry_run = 1; + log_level = APLOG_TRACE1; + ap_log_error( APLOG_MARK, log_level, 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) @@ -1024,278 +872,478 @@ static apr_status_t md_post_config(apr_pool_t *p, apr_pool_t *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; + mc->dry_run = dry_run; + + md_event_init(p); + md_event_subscribe(on_event, 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; + rv = setup_store(&store, mc, p, s); + if (APR_SUCCESS != rv) goto leave; + + rv = md_reg_create(&mc->reg, p, store, mc->proxy_url, mc->ca_certs, + mc->min_delay, mc->retry_failover, + mc->use_store_locks, mc->lock_wait_timeout); + if (APR_SUCCESS != rv) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10072) "setup md registry"); + goto leave; } - - 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. + + /* renew on 30% remaining /*/ + rv = md_ocsp_reg_make(&mc->ocsp, p, store, mc->ocsp_renew_window, + AP_SERVER_BASEVERSION, mc->proxy_url, + mc->min_delay); + if (APR_SUCCESS != rv) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10196) "setup ocsp registry"); + goto leave; + } + + init_ssl(); + + /* How to bootstrap this module: + * 1. find out if we know if http: and/or https: requests will arrive + * 2. apply the now complete configuration settings to the MDs + * 3. Link MDs to the server_recs they are used in. Detect unused MDs. + * 4. Update the store with the MDs. Change domain names, create new MDs, etc. + * Basically all MD properties that are configured directly. + * WARNING: this may change the name of an MD. If an MD loses the first + * of its domain names, it first gets the new first one as name. The + * store will find the old settings and "recover" the previous name. + * 5. Load any staged data from previous driving. + * 6. on a dry run, this is all we do + * 7. Read back the MD properties that reflect the existence and aspect of + * credentials that are in the store (or missing there). + * Expiry times, MD state, etc. + * 8. Determine the list of MDs that need driving/supervision. + * 9. Cleanup any left-overs in registry/store that are no longer needed for + * the list of MDs as we know it now. + * 10. If this list is non-empty, setup a watchdog to run. */ - drive_names = apr_array_make(ptemp, mc->mds->nelts+1, sizeof(const char *)); + /*1*/ + if (APR_SUCCESS != (rv = detect_supported_protocols(mc, s, p, log_level))) goto leave; + /*2*/ + if (APR_SUCCESS != (rv = merge_mds_with_conf(mc, p, s, log_level))) goto leave; + /*3*/ + if (APR_SUCCESS != (rv = link_mds_to_servers(mc, s, p))) goto leave; + /*4*/ + if (APR_SUCCESS != (rv = md_reg_lock_global(mc->reg, ptemp))) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10398) + "unable to obtain global registry lock, " + "renewed certificates may remain inactive on " + "this httpd instance!"); + /* FIXME: or should we fail the server start/reload here? */ + rv = APR_SUCCESS; + goto leave; + } + if (APR_SUCCESS != (rv = md_reg_sync_start(mc->reg, mc->mds, ptemp))) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10073) + "syncing %d mds to registry", mc->mds->nelts); + goto leave; + } + /*5*/ + md_reg_load_stagings(mc->reg, mc->mds, mc->env, p); +leave: + md_reg_unlock_global(mc->reg, ptemp); + return rv; +} + +static apr_status_t md_post_config_after_ssl(apr_pool_t *p, apr_pool_t *plog, + apr_pool_t *ptemp, server_rec *s) +{ + md_srv_conf_t *sc; + apr_status_t rv = APR_SUCCESS; + md_mod_conf_t *mc; + int watched, i; + md_t *md; + + (void)ptemp; + (void)plog; + sc = md_config_get(s); + + /*6*/ + if (!sc || !sc->mc || sc->mc->dry_run) goto leave; + mc = sc->mc; + + /*7*/ + if (APR_SUCCESS != (rv = check_invalid_duplicates(s))) { + goto leave; + } + apr_array_clear(mc->unused_names); 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; + md = APR_ARRAY_IDX(mc->mds, i, md_t *); + + ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "md{%s}: auto_add", md->name); + if (APR_SUCCESS != (rv = auto_add_domains(md, s, p))) { + goto leave; + } + init_acme_tls_1_domains(md, s); + ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "md{%s}: check_usage", md->name); + if (APR_SUCCESS != (rv = check_usage(mc, md, s, p, ptemp))) { + goto leave; + } + ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "md{%s}: sync_finish", md->name); + if (APR_SUCCESS != (rv = md_reg_sync_finish(mc->reg, md, p, ptemp))) { + ap_log_error( APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10172) + "md[%s]: error syncing to store", md->name); + goto leave; } } - - 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) { + /*8*/ + ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "init_cert_watch"); + watched = init_cert_watch_status(mc, p, ptemp, s); + /*9*/ + ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "cleanup challenges"); + md_reg_cleanup_challenges(mc->reg, p, ptemp, mc->mds); + + /* From here on, the domains in the registry are readonly + * and only staging/challenges may be manipulated */ + md_reg_freeze_domains(mc->reg, mc->mds); + + if (watched) { + /*10*/ 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); + "%d out of %d mds need watching", watched, mc->mds->nelts); + md_http_use_implementation(md_curl_get_impl(p)); - rv = start_watchdog(drive_names, p, reg, s, mc); + rv = md_renew_start_watching(mc, s, p); } else { - ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10075) - "no mds to auto drive, no watchdog needed"); + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10075) "no mds to supervise"); } -out: + + if (!mc->ocsp || md_ocsp_count(mc->ocsp) == 0) { + ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, s, "no ocsp to manage"); + goto leave; + } + + md_http_use_implementation(md_curl_get_impl(p)); + rv = md_ocsp_start_watching(mc, s, p); + +leave: + ap_log_error( APLOG_MARK, APLOG_TRACE2, rv, s, "post_config done"); return rv; } /**************************************************************************************************/ -/* Access API to other httpd components */ +/* connection context */ + +typedef struct { + const char *protocol; +} md_conn_ctx; + +static const char *md_protocol_get(const conn_rec *c) +{ + md_conn_ctx *ctx; + + ctx = (md_conn_ctx*)ap_get_module_config(c->conn_config, &md_module); + return ctx? ctx->protocol : NULL; +} + +/**************************************************************************************************/ +/* ALPN handling */ + +static int md_protocol_propose(conn_rec *c, request_rec *r, + server_rec *s, + const apr_array_header_t *offers, + apr_array_header_t *proposals) +{ + (void)s; + if (!r && offers && ap_ssl_conn_is_ssl(c) + && ap_array_str_contains(offers, PROTO_ACME_TLS_1)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "proposing protocol '%s'", PROTO_ACME_TLS_1); + APR_ARRAY_PUSH(proposals, const char*) = PROTO_ACME_TLS_1; + return OK; + } + return DECLINED; +} -static int md_is_managed(server_rec *s) +static int md_protocol_switch(conn_rec *c, request_rec *r, server_rec *s, + const char *protocol) { - md_srv_conf_t *conf = md_config_get(s); + md_conn_ctx *ctx; - 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; + (void)s; + if (!r && ap_ssl_conn_is_ssl(c) && !strcmp(PROTO_ACME_TLS_1, protocol)) { + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "switching protocol '%s'", PROTO_ACME_TLS_1); + ctx = apr_pcalloc(c->pool, sizeof(*ctx)); + ctx->protocol = PROTO_ACME_TLS_1; + ap_set_module_config(c->conn_config, &md_module, ctx); + + c->keepalive = AP_CONN_CLOSE; + return OK; } - ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, - "server %s is not managed", s->server_hostname); - return 0; + return DECLINED; } -static apr_status_t setup_fallback_cert(md_store_t *store, const md_t *md, - server_rec *s, apr_pool_t *p) + +/**************************************************************************************************/ +/* Access API to other httpd components */ + +static void fallback_fnames(apr_pool_t *p, md_pkey_spec_t *kspec, char **keyfn, char **certfn ) +{ + *keyfn = apr_pstrcat(p, "fallback-", md_pkey_filename(kspec, p), NULL); + *certfn = apr_pstrcat(p, "fallback-", md_chain_filename(kspec, p), NULL); +} + +static apr_status_t make_fallback_cert(md_store_t *store, const md_t *md, md_pkey_spec_t *kspec, + server_rec *s, apr_pool_t *p, char *keyfn, char *crtfn) { 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", + + if (APR_SUCCESS != (rv = md_pkey_gen(&pkey, p, kspec)) + || APR_SUCCESS != (rv = md_store_save(store, p, MD_SG_DOMAINS, md->name, + keyfn, MD_SV_PKEY, (void*)pkey, 0)) + || APR_SUCCESS != (rv = 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); + || APR_SUCCESS != (rv = md_store_save(store, p, MD_SG_DOMAINS, md->name, + crtfn, MD_SV_CERT, (void*)cert, 0))) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10174) + "%s: make fallback %s certificate", md->name, md_pkey_spec_name(kspec)); } 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) +static apr_status_t get_certificates(server_rec *s, apr_pool_t *p, int fallback, + apr_array_header_t **pcert_files, + apr_array_header_t **pkey_files) { - apr_status_t rv = APR_ENOENT; + 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; + apr_array_header_t *key_files, *chain_files; + const char *keyfile, *chainfile; + int i; + + *pkey_files = *pcert_files = NULL; + key_files = apr_array_make(p, 5, sizeof(const char*)); + chain_files = apr_array_make(p, 5, sizeof(const char*)); ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10113) - "md_get_certificate called for vhost %s.", s->server_hostname); + "get_certificates 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", + ap_log_error(APLOG_MARK, APLOG_TRACE2, 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); + + sc->is_ssl = 1; + + if (!sc->assigned) { + /* With the new hooks in mod_ssl, we are invoked for all server_rec. It is + * therefore normal, when we have nothing to add here. */ 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); + else if (sc->assigned->nelts != 1) { + if (!fallback) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(10238) + "conflict: %d MDs match Virtualhost %s which uses SSL, however " + "there can be at most 1.", + (int)sc->assigned->nelts, s->server_hostname); + } + return APR_EINVAL; + } + md = APR_ARRAY_IDX(sc->assigned, 0, const md_t*); + + if (md->cert_files && md->cert_files->nelts) { + apr_array_cat(chain_files, md->cert_files); + apr_array_cat(key_files, md->pkey_files); + rv = APR_SUCCESS; + } + else { + md_pkey_spec_t *spec; - 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))) { + for (i = 0; i < md_cert_count(md); ++i) { + spec = md_pkeys_spec_get(md->pks, i); + rv = md_reg_get_cred_files(&keyfile, &chainfile, reg, MD_SG_DOMAINS, md, spec, p); + if (APR_SUCCESS == rv) { + APR_ARRAY_PUSH(key_files, const char*) = keyfile; + APR_ARRAY_PUSH(chain_files, const char*) = chainfile; + } + else if (APR_STATUS_IS_ENOENT(rv)) { + /* certificate for this pkey is not available, others might + * if pkeys have been added for a running mdomain. + * see issue #260 */ + rv = APR_SUCCESS; + } + else if (!APR_STATUS_IS_ENOENT(rv)) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10110) + "retrieving credentials for MD %s (%s)", + md->name, md_pkey_spec_name(spec)); 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); + + if (md_array_is_empty(key_files)) { + if (fallback) { + /* 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. */ + char *kfn, *cfn; + + store = md_reg_store_get(reg); + assert(store); + + for (i = 0; i < md_cert_count(md); ++i) { + spec = md_pkeys_spec_get(md->pks, i); + fallback_fnames(p, spec, &kfn, &cfn); + + md_store_get_fname(&keyfile, store, MD_SG_DOMAINS, md->name, kfn, p); + md_store_get_fname(&chainfile, store, MD_SG_DOMAINS, md->name, cfn, p); + if (!md_file_exists(keyfile, p) || !md_file_exists(chainfile, p)) { + if (APR_SUCCESS != (rv = make_fallback_cert(store, md, spec, s, p, kfn, cfn))) { + return rv; + } + } + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10116) + "%s: providing %s fallback certificate for server %s", + md->name, md_pkey_spec_name(spec), s->server_hostname); + APR_ARRAY_PUSH(key_files, const char*) = keyfile; + APR_ARRAY_PUSH(chain_files, const char*) = chainfile; + } + rv = APR_EAGAIN; + goto leave; + } + } + } + ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, APLOGNO(10077) + "%s[state=%d]: providing certificates for server %s", + md->name, md->state, s->server_hostname); +leave: + if (!md_array_is_empty(key_files) && !md_array_is_empty(chain_files)) { + *pkey_files = key_files; + *pcert_files = chain_files; + } + else if (APR_SUCCESS == rv) { + rv = APR_ENOENT; + } 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) +static int md_add_cert_files(server_rec *s, apr_pool_t *p, + apr_array_header_t *cert_files, + apr_array_header_t *key_files) { - *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); + apr_array_header_t *md_cert_files; + apr_array_header_t *md_key_files; + apr_status_t rv; + + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, "hook ssl_add_cert_files for %s", + s->server_hostname); + rv = get_certificates(s, p, 0, &md_cert_files, &md_key_files); + if (APR_SUCCESS == rv) { + if (!apr_is_empty_array(cert_files)) { + /* downgraded fromm WARNING to DEBUG, since installing separate certificates + * may be a valid use case. */ + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10084) + "host '%s' is covered by a Managed Domain, but " + "certificate/key files are already configured " + "for it (most likely via SSLCertificateFile).", + s->server_hostname); + } + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, + "host '%s' is covered by a Managed Domaina and " + "is being provided with %d key/certificate files.", + s->server_hostname, md_cert_files->nelts); + apr_array_cat(cert_files, md_cert_files); + apr_array_cat(key_files, md_key_files); + return DONE; + } + return DECLINED; } -static int md_is_challenge(conn_rec *c, const char *servername, - X509 **pcert, EVP_PKEY **pkey) +static int md_add_fallback_cert_files(server_rec *s, apr_pool_t *p, + apr_array_header_t *cert_files, + apr_array_header_t *key_files) { - md_srv_conf_t *sc; - apr_size_t slen, sufflen = sizeof(MD_TLSSNI01_DNS_SUFFIX) - 1; + apr_array_header_t *md_cert_files; + apr_array_header_t *md_key_files; apr_status_t rv; - slen = strlen(servername); - if (slen <= sufflen - || apr_strnatcasecmp(MD_TLSSNI01_DNS_SUFFIX, servername + slen - sufflen)) { - return 0; + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, "hook ssl_add_fallback_cert_files for %s", + s->server_hostname); + rv = get_certificates(s, p, 1, &md_cert_files, &md_key_files); + if (APR_EAGAIN == rv) { + apr_array_cat(cert_files, md_cert_files); + apr_array_cat(key_files, md_key_files); + return DONE; + } + return DECLINED; +} + +static int md_answer_challenge(conn_rec *c, const char *servername, + const char **pcert_pem, const char **pkey_pem) +{ + const char *protocol; + int hook_rv = DECLINED; + apr_status_t rv = APR_ENOENT; + md_srv_conf_t *sc; + md_store_t *store; + char *cert_name, *pkey_name; + const char *cert_pem, *key_pem; + int i; + + if (!servername + || !(protocol = md_protocol_get(c)) + || strcmp(PROTO_ACME_TLS_1, protocol)) { + goto cleanup; } - 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); - } + if (!sc || !sc->mc->reg) goto cleanup; + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "Answer challenge[tls-alpn-01] for %s", servername); + store = md_reg_store_get(sc->mc->reg); + + for (i = 0; i < md_pkeys_spec_count( sc->pks ); i++) { + tls_alpn01_fnames(c->pool, md_pkeys_spec_get(sc->pks,i), + &pkey_name, &cert_name); + + rv = md_store_load(store, MD_SG_CHALLENGES, servername, cert_name, MD_SV_TEXT, + (void**)&cert_pem, c->pool); + if (APR_STATUS_IS_ENOENT(rv)) continue; + if (APR_SUCCESS != rv) goto cleanup; + + rv = md_store_load(store, MD_SG_CHALLENGES, servername, pkey_name, MD_SV_TEXT, + (void**)&key_pem, c->pool); + if (APR_STATUS_IS_ENOENT(rv)) continue; + if (APR_SUCCESS != rv) goto cleanup; + + ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c, + "Found challenge cert %s, key %s for %s", + cert_name, pkey_name, servername); + *pcert_pem = cert_pem; + *pkey_pem = key_pem; + hook_rv = OK; + break; + } + + if (DECLINED == hook_rv) { + ap_log_cerror(APLOG_MARK, APLOG_INFO, rv, c, APLOGNO(10080) + "%s: unknown tls-alpn-01 challenge host", servername); } - *pcert = NULL; - *pkey = NULL; - return 0; + +cleanup: + return hook_rv; } + /**************************************************************************************************/ -/* ACME challenge responses */ +/* ACME 'http-01' challenge responses */ #define WELL_KNOWN_PREFIX "/.well-known/" #define ACME_CHALLENGE_PREFIX WELL_KNOWN_PREFIX"acme-challenge/" @@ -1306,30 +1354,39 @@ static int md_http_challenge_pr(request_rec *r) const md_srv_conf_t *sc; const char *name, *data; md_reg_t *reg; - int configured; + const md_t *md; apr_status_t rv; - - if (r->parsed_uri.path + + 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", + 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)); + md = 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 (md && md->ca_challenges + && md_array_str_index(md->ca_challenges, MD_AUTHZ_CHA_HTTP_01, 0, 1) < 0) { + /* The MD this challenge is for does not allow http-01 challanges, + * we have to decline. See #279 for a setup example where this + * is necessary. + */ + return DECLINED; + } + 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, + + 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, + 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; } @@ -1337,29 +1394,31 @@ static int md_http_challenge_pr(request_rec *r) * 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 + else if (!md || md->renew_mode == MD_RENEW_MANUAL + || (md->cert_files && md->cert_files->nelts + && md->renew_mode == MD_RENEW_AUTO)) { + /* The request hostname is not for a domain - or at least not for + * a domain that we renew ourselves. 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. + * So, we decline to handle this and give others a chance to provide + * the answer. */ 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; - } + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(10081) + "loading challenge %s from store", name); + return HTTP_INTERNAL_SERVER_ERROR; } } } @@ -1373,55 +1432,65 @@ static int md_require_https_maybe(request_rec *r) { const md_srv_conf_t *sc; apr_uri_t uri; - const char *s; + const char *s, *host; + const md_t *md; 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); - } + + /* Requests outside the /.well-known path are subject to possible + * https: redirects or HSTS header additions. + */ + sc = ap_get_module_config(r->server->module_config, &md_module); + if (!sc || !sc->assigned || !sc->assigned->nelts || !r->parsed_uri.path + || !strncmp(WELL_KNOWN_PREFIX, r->parsed_uri.path, sizeof(WELL_KNOWN_PREFIX)-1)) { + goto declined; + } + + host = ap_get_server_name_for_url(r); + md = md_get_for_domain(r->server, host); + if (!md) goto declined; + + if (ap_ssl_conn_is_ssl(r->connection)) { + /* Using https: + * if 'permanent' and no one else set a HSTS header already, do it */ + if (md->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 { + if (md->require_https > MD_REQUIRE_OFF) { + /* Not using https:, but require it. Redirect. */ + if (r->method_number == M_GET) { + /* safe to use the old-fashioned codes */ + status = ((MD_REQUIRE_PERMANENT == md->require_https)? + HTTP_MOVED_PERMANENTLY : HTTP_MOVED_TEMPORARILY); } 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; - } + /* these should keep the method unchanged on retry */ + status = ((MD_REQUIRE_PERMANENT == md->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; } } } } +declined: return DECLINED; } -/* Runs once per created child process. Perform any process +/* Runs once per created child process. Perform any process * related initialization here. */ static void md_child_init(apr_pool_t *pool, server_rec *s) @@ -1434,27 +1503,47 @@ static void md_child_init(apr_pool_t *pool, server_rec *s) */ static void md_hooks(apr_pool_t *pool) { - static const char *const mod_ssl[] = { "mod_ssl.c", NULL}; + static const char *const mod_ssl[] = { "mod_ssl.c", "mod_tls.c", NULL}; + static const char *const mod_wd[] = { "mod_watchdog.c", NULL}; + + /* Leave the ssl initialization to mod_ssl or friends. */ + md_acme_init(pool, AP_SERVER_BASEVERSION, 0); - 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. + * Run again after mod_ssl is done. */ - ap_hook_post_config(md_post_config, NULL, mod_ssl, APR_HOOK_MIDDLE); - + ap_hook_post_config(md_post_config_before_ssl, NULL, mod_ssl, APR_HOOK_MIDDLE); + ap_hook_post_config(md_post_config_after_ssl, mod_ssl, mod_wd, APR_HOOK_LAST); + /* 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_require_https_maybe, mod_ssl, NULL, APR_HOOK_MIDDLE); 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); + ap_hook_protocol_propose(md_protocol_propose, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_protocol_switch(md_protocol_switch, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_protocol_get(md_protocol_get, NULL, NULL, APR_HOOK_MIDDLE); + + /* Status request handlers and contributors */ + ap_hook_post_read_request(md_http_cert_status, NULL, mod_ssl, APR_HOOK_MIDDLE); + APR_OPTIONAL_HOOK(ap, status_hook, md_domains_status_hook, NULL, NULL, APR_HOOK_MIDDLE); + APR_OPTIONAL_HOOK(ap, status_hook, md_ocsp_status_hook, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_handler(md_status_handler, NULL, NULL, APR_HOOK_MIDDLE); + + ap_hook_ssl_answer_challenge(md_answer_challenge, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_ssl_add_cert_files(md_add_cert_files, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_ssl_add_fallback_cert_files(md_add_fallback_cert_files, NULL, NULL, APR_HOOK_MIDDLE); + +#if AP_MODULE_MAGIC_AT_LEAST(20120211, 105) + ap_hook_ssl_ocsp_prime_hook(md_ocsp_prime_status, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_ssl_ocsp_get_resp_hook(md_ocsp_provide_status, NULL, NULL, APR_HOOK_MIDDLE); +#else +#error "This version of mod_md requires Apache httpd 2.4.48 or newer." +#endif /* AP_MODULE_MAGIC_AT_LEAST() */ } diff --git a/modules/md/mod_md.dsp b/modules/md/mod_md.dsp index c685f54..d99fb1c 100644 --- a/modules/md/mod_md.dsp +++ b/modules/md/mod_md.dsp @@ -43,7 +43,7 @@ RSC=rc.exe # 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 CPP /nologo /MD /W3 /O2 /Oy- /Zi /I "../../server/mpm/winnt" "/I ../ssl" /I "../../include" /I "../generators" /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"
@@ -75,7 +75,7 @@ PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).ma # 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 CPP /nologo /MDd /W3 /EHsc /Zi /Od /I "../ssl" /I "../../include" /I "../../srclib/apr/include" /I "../generators" /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"
@@ -109,10 +109,46 @@ SOURCE=./mod_md_config.c # End Source File
# Begin Source File
+SOURCE=./mod_md_drive.c
+# End Source File
+# Begin Source File
+
+SOURCE=./mod_md_ocsp.c
+# End Source File
+# Begin Source File
+
SOURCE=./mod_md_os.c
# End Source File
# Begin Source File
+SOURCE=./mod_md_status.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=./md_acme_order.c
+# End Source File
+# Begin Source File
+
+SOURCE=./md_acmev2_drive.c
+# End Source File
+# Begin Source File
+
SOURCE=./md_core.c
# End Source File
# Begin Source File
@@ -129,6 +165,10 @@ SOURCE=./md_http.c # End Source File
# Begin Source File
+SOURCE=./md_event.c
+# End Source File
+# Begin Source File
+
SOURCE=./md_json.c
# End Source File
# Begin Source File
@@ -141,38 +181,41 @@ SOURCE=./md_log.c # End Source File
# Begin Source File
-SOURCE=./md_reg.c
+SOURCE=./md_ocsp.c
# End Source File
# Begin Source File
-SOURCE=./md_store.c
+SOURCE=./md_reg.c
# End Source File
# Begin Source File
-SOURCE=./md_store_fs.c
+SOURCE=./md_result.c
# End Source File
# Begin Source File
-SOURCE=./md_util.c
+SOURCE=./md_status.c
# End Source File
# Begin Source File
-SOURCE=./md_acme.c
+SOURCE=./md_store.c
# End Source File
# Begin Source File
-SOURCE=./md_acme_acct.c
+SOURCE=./md_store_fs.c
# End Source File
# Begin Source File
-SOURCE=./md_acme_authz.c
+SOURCE=./md_tailscale.c
# End Source File
# Begin Source File
-SOURCE=./md_acme_drive.c
+SOURCE=./md_time.c
# End Source File
# Begin Source File
+SOURCE=./md_util.c
+# End Source File
+# Begin Source File
SOURCE=..\..\build\win32\httpd.rc
# End Source File
diff --git a/modules/md/mod_md.h b/modules/md/mod_md.h index 5ff8f52..805737d 100644 --- a/modules/md/mod_md.h +++ b/modules/md/mod_md.h @@ -17,34 +17,4 @@ #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 index 9d5881e..9779e6b 100644 --- a/modules/md/mod_md.mak +++ b/modules/md/mod_md.mak @@ -64,20 +64,31 @@ CLEAN : -@erase "$(INTDIR)\md_acme_acct.obj"
-@erase "$(INTDIR)\md_acme_authz.obj"
-@erase "$(INTDIR)\md_acme_drive.obj"
+ -@erase "$(INTDIR)\md_acme_order.obj"
+ -@erase "$(INTDIR)\md_acmev2_drive.obj"
-@erase "$(INTDIR)\md_core.obj"
-@erase "$(INTDIR)\md_crypt.obj"
-@erase "$(INTDIR)\md_curl.obj"
+ -@erase "$(INTDIR)\md_event.obj"
-@erase "$(INTDIR)\md_http.obj"
-@erase "$(INTDIR)\md_json.obj"
-@erase "$(INTDIR)\md_jws.obj"
-@erase "$(INTDIR)\md_log.obj"
+ -@erase "$(INTDIR)\md_ocsp.obj"
-@erase "$(INTDIR)\md_reg.obj"
+ -@erase "$(INTDIR)\md_result.obj"
+ -@erase "$(INTDIR)\md_status.obj"
-@erase "$(INTDIR)\md_store.obj"
-@erase "$(INTDIR)\md_store_fs.obj"
+ -@erase "$(INTDIR)\md_tailscale.obj"
+ -@erase "$(INTDIR)\md_time.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_drive.obj"
+ -@erase "$(INTDIR)\mod_md_status.obj"
+ -@erase "$(INTDIR)\mod_md_ocsp.obj"
-@erase "$(INTDIR)\mod_md_os.obj"
-@erase "$(INTDIR)\mod_md_src.idb"
-@erase "$(INTDIR)\mod_md_src.pdb"
@@ -90,7 +101,7 @@ CLEAN : 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
+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" /I "../generators" /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) @<<
@@ -135,22 +146,33 @@ LINK32_FLAGS=kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib $(SSLCRP).l LINK32_OBJS= \
"$(INTDIR)\mod_md.obj" \
"$(INTDIR)\mod_md_config.obj" \
+ "$(INTDIR)\mod_md_drive.obj" \
+ "$(INTDIR)\mod_md_ocsp.obj" \
"$(INTDIR)\mod_md_os.obj" \
+ "$(INTDIR)\mod_md_status.obj" \
"$(INTDIR)\md_core.obj" \
"$(INTDIR)\md_crypt.obj" \
"$(INTDIR)\md_curl.obj" \
+ "$(INTDIR)\md_event.obj" \
"$(INTDIR)\md_http.obj" \
"$(INTDIR)\md_json.obj" \
"$(INTDIR)\md_jws.obj" \
"$(INTDIR)\md_log.obj" \
+ "$(INTDIR)\md_ocsp.obj" \
"$(INTDIR)\md_reg.obj" \
+ "$(INTDIR)\md_result.obj" \
+ "$(INTDIR)\md_status.obj" \
"$(INTDIR)\md_store.obj" \
"$(INTDIR)\md_store_fs.obj" \
+ "$(INTDIR)\md_tailscale.obj" \
+ "$(INTDIR)\md_time.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)\md_acme_order.obj" \
+ "$(INTDIR)\md_acmev2_drive.obj" \
"$(INTDIR)\mod_md.res" \
"..\..\srclib\apr\Release\libapr-1.lib" \
"..\..\srclib\apr-util\Release\libaprutil-1.lib" \
@@ -203,20 +225,31 @@ CLEAN : -@erase "$(INTDIR)\md_acme_acct.obj"
-@erase "$(INTDIR)\md_acme_authz.obj"
-@erase "$(INTDIR)\md_acme_drive.obj"
+ -@erase "$(INTDIR)\md_acme_order.obj"
+ -@erase "$(INTDIR)\md_acmev2_drive.obj"
-@erase "$(INTDIR)\md_core.obj"
-@erase "$(INTDIR)\md_crypt.obj"
-@erase "$(INTDIR)\md_curl.obj"
+ -@erase "$(INTDIR)\md_event.obj"
-@erase "$(INTDIR)\md_http.obj"
-@erase "$(INTDIR)\md_json.obj"
-@erase "$(INTDIR)\md_jws.obj"
-@erase "$(INTDIR)\md_log.obj"
+ -@erase "$(INTDIR)\md_ocsp.obj"
-@erase "$(INTDIR)\md_reg.obj"
+ -@erase "$(INTDIR)\md_result.obj"
+ -@erase "$(INTDIR)\md_status.obj"
-@erase "$(INTDIR)\md_store.obj"
-@erase "$(INTDIR)\md_store_fs.obj"
+ -@erase "$(INTDIR)\md_tailscale.obj"
+ -@erase "$(INTDIR)\md_time.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_drive.obj"
+ -@erase "$(INTDIR)\mod_md_status.obj"
+ -@erase "$(INTDIR)\mod_md_ocsp.obj"
-@erase "$(INTDIR)\mod_md_os.obj"
-@erase "$(INTDIR)\mod_md_src.idb"
-@erase "$(INTDIR)\mod_md_src.pdb"
@@ -229,7 +262,7 @@ CLEAN : 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
+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 "../generators" /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) @<<
@@ -274,22 +307,33 @@ LINK32_FLAGS=kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib $(SSLCRP).l LINK32_OBJS= \
"$(INTDIR)\mod_md.obj" \
"$(INTDIR)\mod_md_config.obj" \
+ "$(INTDIR)\mod_md_drive.obj" \
+ "$(INTDIR)\mod_md_ocsp.obj" \
"$(INTDIR)\mod_md_os.obj" \
+ "$(INTDIR)\mod_md_status.obj" \
"$(INTDIR)\md_core.obj" \
"$(INTDIR)\md_crypt.obj" \
"$(INTDIR)\md_curl.obj" \
+ "$(INTDIR)\md_event.obj" \
"$(INTDIR)\md_http.obj" \
"$(INTDIR)\md_json.obj" \
"$(INTDIR)\md_jws.obj" \
"$(INTDIR)\md_log.obj" \
+ "$(INTDIR)\md_ocsp.obj" \
"$(INTDIR)\md_reg.obj" \
+ "$(INTDIR)\md_result.obj" \
+ "$(INTDIR)\md_status.obj" \
"$(INTDIR)\md_store.obj" \
"$(INTDIR)\md_store_fs.obj" \
+ "$(INTDIR)\md_tailscale.obj" \
+ "$(INTDIR)\md_time.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)\md_acme_order.obj" \
+ "$(INTDIR)\md_acmev2_drive.obj" \
"$(INTDIR)\mod_md.res" \
"..\..\srclib\apr\Debug\libapr-1.lib" \
"..\..\srclib\apr-util\Debug\libaprutil-1.lib" \
@@ -445,6 +489,16 @@ SOURCE=./md_acme_drive.c "$(INTDIR)\md_acme_drive.obj" : $(SOURCE) "$(INTDIR)"
+SOURCE=./md_acme_order.c
+
+"$(INTDIR)\md_acme_order.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_acmev2_drive.c
+
+"$(INTDIR)\md_acmev2_drive.obj" : $(SOURCE) "$(INTDIR)"
+
+
SOURCE=./md_core.c
"$(INTDIR)\md_core.obj" : $(SOURCE) "$(INTDIR)"
@@ -460,6 +514,11 @@ SOURCE=./md_curl.c "$(INTDIR)\md_curl.obj" : $(SOURCE) "$(INTDIR)"
+SOURCE=./md_event.c
+
+"$(INTDIR)\md_event.obj" : $(SOURCE) "$(INTDIR)"
+
+
SOURCE=./md_http.c
"$(INTDIR)\md_http.obj" : $(SOURCE) "$(INTDIR)"
@@ -480,11 +539,26 @@ SOURCE=./md_log.c "$(INTDIR)\md_log.obj" : $(SOURCE) "$(INTDIR)"
+SOURCE=./md_ocsp.c
+
+"$(INTDIR)\md_ocsp.obj" : $(SOURCE) "$(INTDIR)"
+
+
SOURCE=./md_reg.c
"$(INTDIR)\md_reg.obj" : $(SOURCE) "$(INTDIR)"
+SOURCE=./md_result.c
+
+"$(INTDIR)\md_result.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_status.c
+
+"$(INTDIR)\md_status.obj" : $(SOURCE) "$(INTDIR)"
+
+
SOURCE=./md_store.c
"$(INTDIR)\md_store.obj" : $(SOURCE) "$(INTDIR)"
@@ -495,6 +569,16 @@ SOURCE=./md_store_fs.c "$(INTDIR)\md_store_fs.obj" : $(SOURCE) "$(INTDIR)"
+SOURCE=./md_tailscale.c
+
+"$(INTDIR)\md_tailscale.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./md_time.c
+
+"$(INTDIR)\md_time.obj" : $(SOURCE) "$(INTDIR)"
+
+
SOURCE=./md_util.c
"$(INTDIR)\md_util.obj" : $(SOURCE) "$(INTDIR)"
@@ -510,11 +594,25 @@ SOURCE=./mod_md_config.c "$(INTDIR)\mod_md_config.obj" : $(SOURCE) "$(INTDIR)"
+SOURCE=./mod_md_drive.c
+
+"$(INTDIR)\mod_md_drive.obj" : $(SOURCE) "$(INTDIR)"
+
+
+SOURCE=./mod_md_ocsp.c
+
+"$(INTDIR)\mod_md_ocsp.obj" : $(SOURCE) "$(INTDIR)"
+
+
SOURCE=./mod_md_os.c
"$(INTDIR)\mod_md_os.obj" : $(SOURCE) "$(INTDIR)"
+SOURCE=./mod_md_status.c
+
+"$(INTDIR)\mod_md_status.obj" : $(SOURCE) "$(INTDIR)"
+
!ENDIF
diff --git a/modules/md/mod_md_config.c b/modules/md/mod_md_config.c index 336a21b..31d06b4 100644 --- a/modules/md/mod_md_config.c +++ b/modules/md/mod_md_config.c @@ -26,70 +26,105 @@ #include <http_vhost.h> #include "md.h" +#include "md_acme.h" #include "md_crypt.h" +#include "md_log.h" +#include "md_json.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 MD_CMD_MD2_SECTION "<MDomain" #define DEF_VAL (-1) +#ifndef MD_DEFAULT_BASE_DIR +#define MD_DEFAULT_BASE_DIR "md" +#endif + +static md_timeslice_t def_ocsp_keep_window = { + 0, + MD_TIME_OCSP_KEEP_NORM, +}; + +static md_timeslice_t def_ocsp_renew_window = { + MD_TIME_LIFE_NORM, + MD_TIME_RENEW_WINDOW_DEF, +}; + /* 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, + NULL, /* list of mds */ +#if AP_MODULE_MAGIC_AT_LEAST(20180906, 2) + NULL, /* base dirm by default state-dir-relative */ +#else + MD_DEFAULT_BASE_DIR, +#endif + NULL, /* proxy url for outgoing http */ + NULL, /* md_reg_t */ + NULL, /* md_ocsp_reg_t */ + 80, /* local http: port */ + 443, /* local https: port */ + -1, /* can http: */ + -1, /* can https: */ + 0, /* manage base server */ + MD_HSTS_MAX_AGE_DEFAULT, /* hsts max-age */ + NULL, /* hsts headers */ + NULL, /* unused names */ + NULL, /* init errors hash */ + NULL, /* notify cmd */ + NULL, /* message cmd */ + NULL, /* env table */ + 0, /* dry_run flag */ + 1, /* server_status_enabled */ + 1, /* certificate_status_enabled */ + &def_ocsp_keep_window, /* default time to keep ocsp responses */ + &def_ocsp_renew_window, /* default time to renew ocsp responses */ + "crt.sh", /* default cert checker site name */ + "https://crt.sh?q=", /* default cert checker site url */ + NULL, /* CA cert file to use */ + apr_time_from_sec(5), /* minimum delay for retries */ + 13, /* retry_failover after 14 errors, with 5s delay ~ half a day */ + 0, /* store locks, disabled by default */ + apr_time_from_sec(5), /* max time to wait to obaint a store lock */ + MD_MATCH_ALL, /* match vhost severname and aliases */ +}; + +static md_timeslice_t def_renew_window = { + MD_TIME_LIFE_NORM, + MD_TIME_RENEW_WINDOW_DEF, +}; +static md_timeslice_t def_warn_window = { + MD_TIME_LIFE_NORM, + MD_TIME_WARN_WINDOW_DEF, }; /* 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, + "default", /* name */ + NULL, /* server_rec */ + &defmc, /* mc */ + 1, /* transitive */ + MD_REQUIRE_OFF, /* require https */ + MD_RENEW_AUTO, /* renew mode */ + 0, /* must staple */ + NULL, /* pkey spec */ + &def_renew_window, /* renew window */ + &def_warn_window, /* warn window */ + NULL, /* ca urls */ + NULL, /* ca contact (email) */ + MD_PROTO_ACME, /* ca protocol */ + NULL, /* ca agreemnent */ + NULL, /* ca challenges array */ + NULL, /* ca eab kid */ + NULL, /* ca eab hmac */ + 0, /* stapling */ + 1, /* staple others */ + NULL, /* dns01_cmd */ + NULL, /* currently defined md */ + NULL, /* assigned md, post config */ + 0, /* is_ssl, set during mod_ssl post_config */ }; static md_mod_conf_t *mod_md_config; @@ -112,7 +147,9 @@ static md_mod_conf_t *md_mod_conf_get(apr_pool_t *pool, int create) 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 *)); - + mod_md_config->env = apr_table_make(pool, 10); + mod_md_config->init_errors = apr_hash_make(pool); + apr_pool_cleanup_register(pool, NULL, cleanup_mod_config, apr_pool_cleanup_null); } @@ -125,46 +162,66 @@ 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->renew_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->pks = NULL; + sc->renew_window = NULL; + sc->warn_window = NULL; + sc->ca_urls = NULL; + sc->ca_contact = NULL; sc->ca_proto = NULL; sc->ca_agreement = NULL; sc->ca_challenges = NULL; + sc->ca_eab_kid = NULL; + sc->ca_eab_hmac = NULL; + sc->stapling = DEF_VAL; + sc->staple_others = DEF_VAL; + sc->dns01_cmd = 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->renew_mode = from->renew_mode; to->must_staple = from->must_staple; - to->pkey_spec = from->pkey_spec; - to->renew_norm = from->renew_norm; + to->pks = from->pks; + to->warn_window = from->warn_window; to->renew_window = from->renew_window; - to->ca_url = from->ca_url; + to->ca_urls = from->ca_urls; + to->ca_contact = from->ca_contact; to->ca_proto = from->ca_proto; to->ca_agreement = from->ca_agreement; to->ca_challenges = from->ca_challenges; + to->ca_eab_kid = from->ca_eab_kid; + to->ca_eab_hmac = from->ca_eab_hmac; + to->stapling = from->stapling; + to->staple_others = from->staple_others; + to->dns01_cmd = from->dns01_cmd; } 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->renew_mode != DEF_VAL) md->renew_mode = from->renew_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->pks) md->pks = md_pkeys_spec_clone(p, from->pks); + if (from->renew_window) md->renew_window = from->renew_window; + if (from->warn_window) md->warn_window = from->warn_window; + if (from->ca_urls) md->ca_urls = apr_array_copy(p, from->ca_urls); if (from->ca_proto) md->ca_proto = from->ca_proto; if (from->ca_agreement) md->ca_agreement = from->ca_agreement; + if (from->ca_contact) { + apr_array_clear(md->contacts); + APR_ARRAY_PUSH(md->contacts, const char *) = + md_util_schemify(p, from->ca_contact, "mailto"); + } if (from->ca_challenges) md->ca_challenges = apr_array_copy(p, from->ca_challenges); + if (from->ca_eab_kid) md->ca_eab_kid = from->ca_eab_kid; + if (from->ca_eab_hmac) md->ca_eab_hmac = from->ca_eab_hmac; + if (from->stapling != DEF_VAL) md->stapling = from->stapling; + if (from->dns01_cmd) md->dns01_cmd = from->dns01_cmd; } void *md_config_create_svr(apr_pool_t *pool, server_rec *s) @@ -190,23 +247,28 @@ static void *md_config_merge(apr_pool_t *pool, void *basev, void *addv) 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->renew_mode = (add->renew_mode != DEF_VAL)? add->renew_mode : base->renew_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->pks = (!md_pkeys_spec_is_empty(add->pks))? add->pks : base->pks; + nsc->renew_window = add->renew_window? add->renew_window : base->renew_window; + nsc->warn_window = add->warn_window? add->warn_window : base->warn_window; - nsc->ca_url = add->ca_url? add->ca_url : base->ca_url; + nsc->ca_urls = add->ca_urls? apr_array_copy(pool, add->ca_urls) + : (base->ca_urls? apr_array_copy(pool, base->ca_urls) : NULL); + nsc->ca_contact = add->ca_contact? add->ca_contact : base->ca_contact; 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->ca_eab_kid = add->ca_eab_kid? add->ca_eab_kid : base->ca_eab_kid; + nsc->ca_eab_hmac = add->ca_eab_hmac? add->ca_eab_hmac : base->ca_eab_hmac; + nsc->stapling = (add->stapling != DEF_VAL)? add->stapling : base->stapling; + nsc->staple_others = (add->staple_others != DEF_VAL)? add->staple_others : base->staple_others; + nsc->dns01_cmd = (add->dns01_cmd)? add->dns01_cmd : base->dns01_cmd; nsc->current = NULL; - nsc->assigned = NULL; return nsc; } @@ -227,7 +289,7 @@ static int inside_section(cmd_parms *cmd, const char *section) { } static int inside_md_section(cmd_parms *cmd) { - return (inside_section(cmd, MD_CMD_MD_SECTION) || inside_section(cmd, MD_CMD_MD_OLD_SECTION)); + return (inside_section(cmd, MD_CMD_MD_SECTION) || inside_section(cmd, MD_CMD_MD2_SECTION)); } static const char *md_section_check(cmd_parms *cmd) { @@ -238,6 +300,46 @@ static const char *md_section_check(cmd_parms *cmd) { return NULL; } +#define MD_LOC_GLOBAL (0x01) +#define MD_LOC_MD (0x02) +#define MD_LOC_ELSE (0x04) +#define MD_LOC_ALL (0x07) +#define MD_LOC_NOT_MD (0x102) + +static const char *md_conf_check_location(cmd_parms *cmd, int flags) +{ + if (MD_LOC_GLOBAL == flags) { + return ap_check_cmd_context(cmd, GLOBAL_ONLY); + } + if (MD_LOC_NOT_MD == flags && inside_md_section(cmd)) { + return apr_pstrcat(cmd->pool, cmd->cmd->name, " is not allowed inside an '", + MD_CMD_MD_SECTION, "' context", NULL); + } + if (MD_LOC_MD == flags) { + return md_section_check(cmd); + } + else if ((MD_LOC_MD & flags) && inside_md_section(cmd)) { + return NULL; + } + return ap_check_cmd_context(cmd, NOT_IN_DIRECTORY|NOT_IN_LOCATION); +} + +static const char *set_on_off(int *pvalue, const char *s, apr_pool_t *p) +{ + if (!apr_strnatcasecmp("off", s)) { + *pvalue = 0; + } + else if (!apr_strnatcasecmp("on", s)) { + *pvalue = 1; + } + else { + return apr_pstrcat(p, "unknown '", s, + "', supported parameter values are 'on' and 'off'", 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) { @@ -269,7 +371,7 @@ static const char *md_config_sec_start(cmd_parms *cmd, void *mconfig, const char int transitive = -1; (void)mconfig; - if ((err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) { return err; } @@ -284,11 +386,11 @@ static const char *md_config_sec_start(cmd_parms *cmd, void *mconfig, const char return MD_CMD_MD_SECTION " > section must specify a unique domain name"; } - name = ap_getword_white(cmd->pool, &arg); + name = ap_getword_conf(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); + name = ap_getword_conf(cmd->pool, &arg); if (NULL != set_transitive(&transitive, name)) { add_domain_name(domains, name, cmd->pool); } @@ -355,8 +457,7 @@ static const char *md_config_set_names(cmd_parms *cmd, void *dc, int i, transitive = -1; (void)dc; - err = ap_check_cmd_context(cmd, NOT_IN_DIR_LOC_FILE); - if (err) { + if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) { return err; } @@ -385,16 +486,42 @@ static const char *md_config_set_names(cmd_parms *cmd, void *dc, return NULL; } -static const char *md_config_set_ca(cmd_parms *cmd, void *dc, const char *value) +static const char *md_config_set_ca(cmd_parms *cmd, void *dc, + int argc, char *const argv[]) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err, *url; + int i; + + (void)dc; + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; + } + if (!sc->ca_urls) { + sc->ca_urls = apr_array_make(cmd->pool, 3, sizeof(const char *)); + } + else { + apr_array_clear(sc->ca_urls); + } + for (i = 0; i < argc; ++i) { + if (APR_SUCCESS != md_get_ca_url_from_name(&url, cmd->pool, argv[i])) { + return url; + } + APR_ARRAY_PUSH(sc->ca_urls, const char *) = url; + } + return NULL; +} + +static const char *md_config_set_contact(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))) { + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { return err; } - sc->ca_url = value; + sc->ca_contact = value; return NULL; } @@ -404,7 +531,7 @@ static const char *md_config_set_ca_proto(cmd_parms *cmd, void *dc, const char * const char *err; (void)dc; - if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { return err; } config->ca_proto = value; @@ -417,37 +544,37 @@ static const char *md_config_set_agreement(cmd_parms *cmd, void *dc, const char const char *err; (void)dc; - if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { return err; } config->ca_agreement = value; return NULL; } -static const char *md_config_set_drive_mode(cmd_parms *cmd, void *dc, const char *value) +static const char *md_config_set_renew_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; + md_renew_mode_t renew_mode; (void)dc; if (!apr_strnatcasecmp("auto", value) || !apr_strnatcasecmp("automatic", value)) { - drive_mode = MD_DRIVE_AUTO; + renew_mode = MD_RENEW_AUTO; } else if (!apr_strnatcasecmp("always", value)) { - drive_mode = MD_DRIVE_ALWAYS; + renew_mode = MD_RENEW_ALWAYS; } else if (!apr_strnatcasecmp("manual", value) || !apr_strnatcasecmp("stick", value)) { - drive_mode = MD_DRIVE_MANUAL; + renew_mode = MD_RENEW_MANUAL; } else { return apr_pstrcat(cmd->pool, "unknown MDDriveMode ", value, NULL); } - if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { return err; } - config->drive_mode = drive_mode; + config->renew_mode = renew_mode; return NULL; } @@ -457,54 +584,137 @@ static const char *md_config_set_must_staple(cmd_parms *cmd, void *dc, const cha const char *err; (void)dc; - if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { return err; } + return set_on_off(&config->must_staple, value, cmd->pool); +} - if (!apr_strnatcasecmp("off", value)) { - config->must_staple = 0; +static const char *md_config_set_stapling(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + + (void)dc; + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; } - else if (!apr_strnatcasecmp("on", value)) { - config->must_staple = 1; + return set_on_off(&config->stapling, value, cmd->pool); +} + +static const char *md_config_set_staple_others(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + + (void)dc; + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; } - else { - return apr_pstrcat(cmd->pool, "unknown '", value, - "', supported parameter values are 'on' and 'off'", NULL); + return set_on_off(&config->staple_others, value, cmd->pool); +} + +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 = md_conf_check_location(cmd, MD_LOC_NOT_MD); + + (void)dc; + if (err) return err; + return set_on_off(&config->mc->manage_base_server, value, cmd->pool); +} + +static const char *md_config_set_min_delay(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD); + apr_time_t delay; + + (void)dc; + if (err) return err; + if (md_duration_parse(&delay, value, "s") != APR_SUCCESS) { + return "unrecognized duration format"; } + config->mc->min_delay = delay; return NULL; } -static const char *md_config_set_base_server(cmd_parms *cmd, void *dc, const char *value) +static const char *md_config_set_retry_failover(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); + const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD); + int retry_failover; (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); + if (err) return err; + retry_failover = atoi(value); + if (retry_failover <= 0) { + return "invalid argument, must be a number > 0"; + } + config->mc->retry_failover = retry_failover; + return NULL; +} + +static const char *md_config_set_store_locks(cmd_parms *cmd, void *dc, const char *s) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD); + int use_store_locks; + apr_time_t wait_time = 0; + + (void)dc; + if (err) { + return err; + } + else if (!apr_strnatcasecmp("off", s)) { + use_store_locks = 0; + } + else if (!apr_strnatcasecmp("on", s)) { + use_store_locks = 1; + } + else { + if (md_duration_parse(&wait_time, s, "s") != APR_SUCCESS) { + return "neither 'on', 'off' or a duration specified"; } + use_store_locks = (wait_time != 0); } - return err; + config->mc->use_store_locks = use_store_locks; + if (wait_time) { + config->mc->lock_wait_timeout = wait_time; + } + return NULL; } -static const char *md_config_set_require_https(cmd_parms *cmd, void *dc, const char *value) +static const char *md_config_set_match_mode(cmd_parms *cmd, void *dc, const char *s) { md_srv_conf_t *config = md_config_get(cmd->server); - const char *err; + const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD); (void)dc; - if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + if (err) { return err; } + else if (!apr_strnatcasecmp("all", s)) { + config->mc->match_mode = MD_MATCH_ALL; + } + else if (!apr_strnatcasecmp("servernames", s)) { + config->mc->match_mode = MD_MATCH_SERVERNAMES; + } + else { + return "invalid argument, must be a 'all' or 'servernames'"; + } + return NULL; +} +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; + + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; + } + (void)dc; if (!apr_strnatcasecmp("off", value)) { config->require_https = MD_REQUIRE_OFF; } @@ -521,98 +731,48 @@ static const char *md_config_set_require_https(cmd_parms *cmd, void *dc, const c 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) +static const char *md_config_set_renew_window(cmd_parms *cmd, void *dc, const char *value) { - char *endp; - apr_int64_t n; + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; - n = apr_strtoi64(value, &endp, 10); - if (errno) { - return errno; + (void)dc; + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; } - if (*endp == '%') { - if (n < 0 || n >= 100) { - return APR_BADARG; - } - *ppercent = (int)n; - return APR_SUCCESS; + err = md_timeslice_parse(&config->renew_window, cmd->pool, value, MD_TIME_LIFE_NORM); + if (!err && config->renew_window->norm + && (config->renew_window->len >= config->renew_window->norm)) { + err = "a length of 100% or more is not allowed."; } - return APR_EINVAL; + if (err) return apr_psprintf(cmd->pool, "MDRenewWindow %s", err); + return NULL; } -static const char *md_config_set_renew_window(cmd_parms *cmd, void *dc, const char *value) +static const char *md_config_set_warn_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))) { + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { 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"; - } + err = md_timeslice_parse(&config->warn_window, cmd->pool, value, MD_TIME_LIFE_NORM); + if (!err && config->warn_window->norm + && (config->warn_window->len >= config->warn_window->norm)) { + err = "a length of 100% or more is not allowed."; } - return "MDRenewWindow has unrecognized format"; + if (err) return apr_psprintf(cmd->pool, "MDWarnWindow %s", err); + return NULL; } 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); + const char *err; - if (err) { + if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) { return err; } md_util_abs_http_uri_check(cmd->pool, value, &err); @@ -627,9 +787,9 @@ static const char *md_config_set_proxy(cmd_parms *cmd, void *arg, const char *va 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); + const char *err; - if (err) { + if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) { return err; } sc->mc->base_dir = value; @@ -640,11 +800,19 @@ static const char *md_config_set_store_dir(cmd_parms *cmd, void *arg, const char static const char *set_port_map(md_mod_conf_t *mc, const char *value) { int net_port, local_port; - char *endp; + const char *endp; - net_port = (int)apr_strtoi64(value, &endp, 10); - if (errno) { - return "unable to parse first port number"; + if (!strncmp("http:", value, sizeof("http:") - 1)) { + net_port = 80; endp = value + sizeof("http") - 1; + } + else if (!strncmp("https:", value, sizeof("https:") - 1)) { + net_port = 443; endp = value + sizeof("https") - 1; + } + else { + net_port = (int)apr_strtoi64(value, (char**)&endp, 10); + if (errno) { + return "unable to parse first port number"; + } } if (!endp || *endp != ':') { return "no ':' after first port number"; @@ -654,7 +822,7 @@ static const char *set_port_map(md_mod_conf_t *mc, const char *value) local_port = 0; } else { - local_port = (int)apr_strtoi64(endp, &endp, 10); + local_port = (int)apr_strtoi64(endp, (char**)&endp, 10); if (errno) { return "unable to parse second port number"; } @@ -679,10 +847,10 @@ 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); + const char *err; (void)arg; - if (!err) { + if (!(err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) { err = set_port_map(sc->mc, v1); } if (!err && v2) { @@ -700,14 +868,16 @@ static const char *md_config_set_cha_tyes(cmd_parms *cmd, void *dc, int i; (void)dc; - if (!inside_md_section(cmd) - && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { return err; } pcha = &config->ca_challenges; ca_challenges = *pcha; - if (!ca_challenges) { + if (ca_challenges) { + apr_array_clear(ca_challenges); + } + else { *pcha = ca_challenges = apr_array_make(cmd->pool, 5, sizeof(const char *)); } for (i = 0; i < argc; ++i) { @@ -723,60 +893,81 @@ static const char *md_config_set_pkeys(cmd_parms *cmd, void *dc, md_srv_conf_t *config = md_config_get(cmd->server); const char *err, *ptype; apr_int64_t bits; + int i; (void)dc; - if (!inside_md_section(cmd) - && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { 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->pks = md_pkeys_spec_make(cmd->pool); + for (i = 0; i < argc; ++i) { + ptype = argv[i]; + if (!apr_strnatcasecmp("Default", ptype)) { + if (argc > 1) { + return "'Default' allows no other parameter"; + } + md_pkeys_spec_add_default(config->pks); } - 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 (strlen(ptype) > 3 + && (ptype[0] == 'R' || ptype[0] == 'r') + && (ptype[1] == 'S' || ptype[1] == 's') + && (ptype[2] == 'A' || ptype[2] == 'a') + && isdigit(ptype[3])) { + bits = (int)apr_atoi64(ptype+3); + if (bits < MD_PKEY_RSA_BITS_MIN) { + return apr_psprintf(cmd->pool, + "must be %d or higher in order to be considered safe.", + MD_PKEY_RSA_BITS_MIN); + } + if (bits >= INT_MAX) { + return apr_psprintf(cmd->pool, "is too large for an RSA key length."); + } + if (md_pkeys_spec_contains_rsa(config->pks)) { + return "two keys of type 'RSA' are not possible."; + } + md_pkeys_spec_add_rsa(config->pks, (unsigned int)bits); } - 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 if (!apr_strnatcasecmp("RSA", ptype)) { + if (i+1 >= argc || !isdigit(argv[i+1][0])) { + bits = MD_PKEY_RSA_BITS_DEF; + } + else { + ++i; + bits = (int)apr_atoi64(argv[i]); + if (bits < MD_PKEY_RSA_BITS_MIN) { + return apr_psprintf(cmd->pool, + "must be %d or higher in order to be considered safe.", + MD_PKEY_RSA_BITS_MIN); + } + if (bits >= INT_MAX) { + return apr_psprintf(cmd->pool, "is too large for an RSA key length."); + } + } + if (md_pkeys_spec_contains_rsa(config->pks)) { + return "two keys of type 'RSA' are not possible."; } + md_pkeys_spec_add_rsa(config->pks, (unsigned int)bits); } 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)); + if (md_pkeys_spec_contains_ec(config->pks, argv[i])) { + return apr_psprintf(cmd->pool, "two keys of type '%s' are not possible.", argv[i]); + } + md_pkeys_spec_add_ec(config->pks, argv[i]); } - 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); + return 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); + const char *err; - if (err) { + if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) { return err; } sc->mc->notify_cmd = arg; @@ -784,70 +975,335 @@ static const char *md_config_set_notify_cmd(cmd_parms *cmd, void *mconfig, const return NULL; } -static const char *md_config_set_names_old(cmd_parms *cmd, void *dc, - int argc, char *const argv[]) +static const char *md_config_set_msg_cmd(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 'MDomain'."); - return md_config_set_names(cmd, dc, argc, argv); + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) { + return err; + } + sc->mc->message_cmd = arg; + (void)mconfig; + return NULL; } -static const char *md_config_sec_start_old(cmd_parms *cmd, void *mconfig, const char *arg) +static const char *md_config_set_dns01_cmd(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); + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; + } + + if (inside_md_section(cmd)) { + sc->dns01_cmd = arg; + } else { + apr_table_set(sc->mc->env, MD_KEY_CMD_DNS01, arg); + } + + (void)mconfig; + return NULL; +} + +static const char *md_config_set_dns01_version(cmd_parms *cmd, void *mconfig, const char *value) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)mconfig; + if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) { + return err; + } + if (!strcmp("1", value) || !strcmp("2", value)) { + apr_table_set(sc->mc->env, MD_KEY_DNS01_VERSION, value); + } + else { + return "Only versions `1` and `2` are supported"; + } + return NULL; +} + +static const char *md_config_add_cert_file(cmd_parms *cmd, void *mconfig, const char *arg) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err, *fpath; + + (void)mconfig; + if ((err = md_conf_check_location(cmd, MD_LOC_MD))) return err; + assert(sc->current); + fpath = ap_server_root_relative(cmd->pool, arg); + if (!fpath) return apr_psprintf(cmd->pool, "certificate file not found: %s", arg); + if (!sc->current->cert_files) { + sc->current->cert_files = apr_array_make(cmd->pool, 3, sizeof(char*)); + } + APR_ARRAY_PUSH(sc->current->cert_files, const char*) = fpath; + return NULL; +} + +static const char *md_config_add_key_file(cmd_parms *cmd, void *mconfig, const char *arg) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err, *fpath; + + (void)mconfig; + if ((err = md_conf_check_location(cmd, MD_LOC_MD))) return err; + assert(sc->current); + fpath = ap_server_root_relative(cmd->pool, arg); + if (!fpath) return apr_psprintf(cmd->pool, "certificate key file not found: %s", arg); + if (!sc->current->pkey_files) { + sc->current->pkey_files = apr_array_make(cmd->pool, 3, sizeof(char*)); + } + APR_ARRAY_PUSH(sc->current->pkey_files, const char*) = fpath; + return NULL; +} + +static const char *md_config_set_server_status(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)dc; + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; + } + return set_on_off(&sc->mc->server_status_enabled, value, cmd->pool); +} + +static const char *md_config_set_certificate_status(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)dc; + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; + } + return set_on_off(&sc->mc->certificate_status_enabled, value, cmd->pool); +} + +static const char *md_config_set_ocsp_keep_window(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)dc; + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; + } + err = md_timeslice_parse(&sc->mc->ocsp_keep_window, cmd->pool, value, MD_TIME_OCSP_KEEP_NORM); + if (err) return apr_psprintf(cmd->pool, "MDStaplingKeepResponse %s", err); + return NULL; +} + +static const char *md_config_set_ocsp_renew_window(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)dc; + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; + } + err = md_timeslice_parse(&sc->mc->ocsp_renew_window, cmd->pool, value, MD_TIME_LIFE_NORM); + if (!err && sc->mc->ocsp_renew_window->norm + && (sc->mc->ocsp_renew_window->len >= sc->mc->ocsp_renew_window->norm)) { + err = "with a length of 100% or more is not allowed."; + } + if (err) return apr_psprintf(cmd->pool, "MDStaplingRenewWindow %s", err); + return NULL; +} + +static const char *md_config_set_cert_check(cmd_parms *cmd, void *dc, + const char *name, const char *url) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)dc; + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; + } + sc->mc->cert_check_name = name; + sc->mc->cert_check_url = url; + return NULL; +} + +static const char *md_config_set_activation_delay(cmd_parms *cmd, void *mconfig, const char *arg) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + apr_interval_time_t delay; + + (void)mconfig; + if ((err = md_conf_check_location(cmd, MD_LOC_NOT_MD))) { + return err; + } + if (md_duration_parse(&delay, arg, "d") != APR_SUCCESS) { + return "unrecognized duration format"; + } + apr_table_set(sc->mc->env, MD_KEY_ACTIVATION_DELAY, md_duration_format(cmd->pool, delay)); + return NULL; +} + +static const char *md_config_set_ca_certs(cmd_parms *cmd, void *dc, const char *path) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + + (void)dc; + sc->mc->ca_certs = path; + return NULL; +} + +static const char *md_config_set_eab(cmd_parms *cmd, void *dc, + const char *keyid, const char *hmac) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)dc; + if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { + return err; + } + if (!hmac) { + if (!apr_strnatcasecmp("None", keyid)) { + keyid = "none"; + } + else { + /* a JSON file keeping keyid and hmac */ + const char *fpath; + apr_status_t rv; + md_json_t *json; + + /* If only dumping the config, don't verify the file */ + if (ap_state_query(AP_SQ_RUN_MODE) == AP_SQ_RM_CONFIG_DUMP) { + goto leave; + } + + fpath = ap_server_root_relative(cmd->pool, keyid); + if (!fpath) { + return apr_pstrcat(cmd->pool, cmd->cmd->name, + ": Invalid file path ", keyid, NULL); + } + if (!md_file_exists(fpath, cmd->pool)) { + return apr_pstrcat(cmd->pool, cmd->cmd->name, + ": file not found: ", fpath, NULL); + } + + rv = md_json_readf(&json, cmd->pool, fpath); + if (APR_SUCCESS != rv) { + return apr_pstrcat(cmd->pool, cmd->cmd->name, + ": error reading JSON file ", fpath, NULL); + } + keyid = md_json_gets(json, MD_KEY_KID, NULL); + if (!keyid || !*keyid) { + return apr_pstrcat(cmd->pool, cmd->cmd->name, + ": JSON does not contain '", MD_KEY_KID, + "' element in file ", fpath, NULL); + } + hmac = md_json_gets(json, MD_KEY_HMAC, NULL); + if (!hmac || !*hmac) { + return apr_pstrcat(cmd->pool, cmd->cmd->name, + ": JSON does not contain '", MD_KEY_HMAC, + "' element in file ", fpath, NULL); + } + } + } +leave: + sc->ca_eab_kid = keyid; + sc->ca_eab_hmac = hmac; + return NULL; } 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, + AP_INIT_TAKE_ARGV("MDCertificateAuthority", md_config_set_ca, NULL, RSRC_CONF, + "URL(s) or known name(s) of CA issuing the certificates"), + AP_INIT_TAKE1("MDCertificateAgreement", md_config_set_agreement, NULL, RSRC_CONF, + "either 'accepted' or the URL of CA Terms-of-Service agreement you accept"), + AP_INIT_TAKE_ARGV("MDCAChallenges", 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, + AP_INIT_TAKE1("MDCertificateProtocol", 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, + AP_INIT_TAKE1("MDContactEmail", md_config_set_contact, NULL, RSRC_CONF, + "Email address used for account registration"), + AP_INIT_TAKE1("MDDriveMode", md_config_set_renew_mode, NULL, RSRC_CONF, + "deprecated, older name for MDRenewMode"), + AP_INIT_TAKE1("MDRenewMode", md_config_set_renew_mode, NULL, RSRC_CONF, + "Controls how renewal of Managed Domain certificates shall be handled."), + AP_INIT_TAKE_ARGV("MDomain", 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, + 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, + AP_INIT_RAW_ARGS(MD_CMD_MD2_SECTION, md_config_sec_start, NULL, RSRC_CONF, + "Short form for <MDomainSet> container."), + AP_INIT_TAKE_ARGV("MDMember", 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, + AP_INIT_TAKE_ARGV("MDMembers", 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, + AP_INIT_TAKE1("MDMustStaple", 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, + AP_INIT_TAKE12("MDPortMap", 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, + AP_INIT_TAKE_ARGV("MDPrivateKeys", 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, + AP_INIT_TAKE1("MDHttpProxy", 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, + AP_INIT_TAKE1("MDStoreDir", 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, + AP_INIT_TAKE1("MDRenewWindow", md_config_set_renew_window, NULL, RSRC_CONF, + "Time length for renewal before certificate expires (defaults to days)."), + AP_INIT_TAKE1("MDRequireHttps", md_config_set_require_https, NULL, RSRC_CONF|OR_AUTHCFG, "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_RAW_ARGS("MDNotifyCmd", md_config_set_notify_cmd, NULL, RSRC_CONF, + "Set the command to run when signup/renew of domain is complete."), + AP_INIT_TAKE1("MDBaseServer", md_config_set_base_server, NULL, RSRC_CONF, + "Allow managing of base server outside virtual hosts."), + AP_INIT_RAW_ARGS("MDChallengeDns01", md_config_set_dns01_cmd, NULL, RSRC_CONF, + "Set the command for setup/teardown of dns-01 challenges"), + AP_INIT_TAKE1("MDChallengeDns01Version", md_config_set_dns01_version, NULL, RSRC_CONF, + "Set the type of arguments to call `MDChallengeDns01` with"), + AP_INIT_TAKE1("MDCertificateFile", md_config_add_cert_file, NULL, RSRC_CONF, + "set the static certificate (chain) file to use for this domain."), + AP_INIT_TAKE1("MDCertificateKeyFile", md_config_add_key_file, NULL, RSRC_CONF, + "set the static private key file to use for this domain."), + AP_INIT_TAKE1("MDServerStatus", md_config_set_server_status, NULL, RSRC_CONF, + "On to see Managed Domains in server-status."), + AP_INIT_TAKE1("MDCertificateStatus", md_config_set_certificate_status, NULL, RSRC_CONF, + "On to see Managed Domain expose /.httpd/certificate-status."), + AP_INIT_TAKE1("MDWarnWindow", md_config_set_warn_window, NULL, RSRC_CONF, + "When less time remains for a certificate, send our/log a warning (defaults to days)"), + AP_INIT_RAW_ARGS("MDMessageCmd", md_config_set_msg_cmd, NULL, RSRC_CONF, + "Set the command run when a message about a domain is issued."), + AP_INIT_TAKE1("MDStapling", md_config_set_stapling, NULL, RSRC_CONF, + "Enable/Disable OCSP Stapling for this/all Managed Domain(s)."), + AP_INIT_TAKE1("MDStapleOthers", md_config_set_staple_others, NULL, RSRC_CONF, + "Enable/Disable OCSP Stapling for certificates not in Managed Domains."), + AP_INIT_TAKE1("MDStaplingKeepResponse", md_config_set_ocsp_keep_window, NULL, RSRC_CONF, + "The amount of time to keep an OCSP response in the store."), + AP_INIT_TAKE1("MDStaplingRenewWindow", md_config_set_ocsp_renew_window, NULL, RSRC_CONF, + "Time length for renewal before OCSP responses expire (defaults to days)."), + AP_INIT_TAKE2("MDCertificateCheck", md_config_set_cert_check, NULL, RSRC_CONF, + "Set name and URL pattern for a certificate monitoring site."), + AP_INIT_TAKE1("MDActivationDelay", md_config_set_activation_delay, NULL, RSRC_CONF, + "How long to delay activation of new certificates"), + AP_INIT_TAKE1("MDCACertificateFile", md_config_set_ca_certs, NULL, RSRC_CONF, + "Set the CA file to use for connections"), + AP_INIT_TAKE12("MDExternalAccountBinding", md_config_set_eab, NULL, RSRC_CONF, + "Set the external account binding keyid and hmac values to use at CA"), + AP_INIT_TAKE1("MDRetryDelay", md_config_set_min_delay, NULL, RSRC_CONF, + "Time length for first retry, doubled on every consecutive error."), + AP_INIT_TAKE1("MDRetryFailover", md_config_set_retry_failover, NULL, RSRC_CONF, + "The number of errors before a failover to another CA is triggered."), + AP_INIT_TAKE1("MDStoreLocks", md_config_set_store_locks, NULL, RSRC_CONF, + "Configure locking of store for updates."), + AP_INIT_TAKE1("MDMatchNames", md_config_set_match_mode, NULL, RSRC_CONF, + "Determines how DNS names are matched to vhosts."), AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL) }; @@ -864,6 +1320,12 @@ apr_status_t md_config_post_config(server_rec *s, apr_pool_t *p) if (mc->hsts_max_age > 0) { mc->hsts_header = apr_psprintf(p, "max-age=%d", mc->hsts_max_age); } + +#if AP_MODULE_MAGIC_AT_LEAST(20180906, 2) + if (mc->base_dir == NULL) { + mc->base_dir = ap_state_dir_relative(p, MD_DEFAULT_BASE_DIR); + } +#endif return APR_SUCCESS; } @@ -874,6 +1336,7 @@ static md_srv_conf_t *config_get_int(server_rec *s, apr_pool_t *p) ap_assert(sc); if (sc->s != s && p) { sc = md_config_merge(p, &defconf, sc); + sc->s = s; 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); @@ -900,8 +1363,8 @@ md_srv_conf_t *md_config_cget(conn_rec *c) 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_CONTACT: + return sc->ca_contact? sc->ca_contact : defconf.ca_contact; case MD_CONFIG_CA_PROTO: return sc->ca_proto? sc->ca_proto : defconf.ca_proto; case MD_CONFIG_BASE_DIR: @@ -921,30 +1384,49 @@ 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; + return (sc->renew_mode != DEF_VAL)? sc->renew_mode : defconf.renew_mode; 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; + case MD_CONFIG_STAPLING: + return (sc->stapling != DEF_VAL)? sc->stapling : defconf.stapling; + case MD_CONFIG_STAPLE_OTHERS: + return (sc->staple_others != DEF_VAL)? sc->staple_others : defconf.staple_others; default: return 0; } } -apr_interval_time_t md_config_get_interval(const md_srv_conf_t *sc, md_config_var_t var) +void md_config_get_timespan(md_timeslice_t **pspan, 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; + *pspan = sc->renew_window? sc->renew_window : defconf.renew_window; + break; + case MD_CONFIG_WARN_WINDOW: + *pspan = sc->warn_window? sc->warn_window : defconf.warn_window; + break; default: - return 0; + break; + } +} + +const md_t *md_get_for_domain(server_rec *s, const char *domain) +{ + md_srv_conf_t *sc; + const md_t *md; + int i; + + sc = md_config_get(s); + for (i = 0; sc && sc->assigned && i < sc->assigned->nelts; ++i) { + md = APR_ARRAY_IDX(sc->assigned, i, const md_t*); + if (md_contains(md, domain, 0)) goto leave; } + md = NULL; +leave: + return md; } + diff --git a/modules/md/mod_md_config.h b/modules/md/mod_md_config.h index 7c7df51..7e87440 100644 --- a/modules/md/mod_md_config.h +++ b/modules/md/mod_md_config.h @@ -17,32 +17,42 @@ #ifndef mod_md_md_config_h #define mod_md_md_config_h +struct apr_hash_t; struct md_store_t; struct md_reg_t; -struct md_pkey_spec_t; +struct md_ocsp_reg_t; +struct md_pkeys_spec_t; typedef enum { - MD_CONFIG_CA_URL, + MD_CONFIG_CA_CONTACT, 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_WARN_WINDOW, MD_CONFIG_TRANSITIVE, MD_CONFIG_PROXY, MD_CONFIG_REQUIRE_HTTPS, MD_CONFIG_MUST_STAPLE, MD_CONFIG_NOTIFY_CMD, + MD_CONFIG_MESSGE_CMD, + MD_CONFIG_STAPLING, + MD_CONFIG_STAPLE_OTHERS, } md_config_var_t; -typedef struct { +typedef enum { + MD_MATCH_ALL, + MD_MATCH_SERVERNAMES, +} md_match_mode_t; + +typedef struct md_mod_conf_t md_mod_conf_t; +struct md_mod_conf_t { 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 */ + struct md_reg_t *reg; /* md registry instance */ + struct md_ocsp_reg_t *ocsp; /* ocsp status registry */ int local_80; /* On which port http:80 arrives */ int local_443; /* On which port https:443 arrives */ @@ -52,9 +62,25 @@ typedef struct { 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 */ + struct apr_hash_t *init_errors; /* init errors reported with MD name as key */ const char *notify_cmd; /* notification command to execute on signup/renew */ -} md_mod_conf_t; + const char *message_cmd; /* message command to execute on signup/renew/warnings */ + struct apr_table_t *env; /* environment for operation */ + int dry_run; /* != 0 iff config dry run */ + int server_status_enabled; /* if module should add to server-status handler */ + int certificate_status_enabled; /* if module should expose /.httpd/certificate-status */ + md_timeslice_t *ocsp_keep_window; /* time that we keep ocsp responses around */ + md_timeslice_t *ocsp_renew_window; /* time before exp. that we start renewing ocsp resp. */ + const char *cert_check_name; /* name of the linked certificate check site */ + const char *cert_check_url; /* url "template for" checking a certificate */ + const char *ca_certs; /* root certificates to use for connections */ + apr_time_t min_delay; /* minimum delay for retries */ + int retry_failover; /* number of errors to trigger CA failover */ + int use_store_locks; /* use locks when updating store */ + apr_time_t lock_wait_timeout; /* fail after this time when unable to obtain lock */ + md_match_mode_t match_mode; /* how dns names are match to vhosts */ +}; typedef struct md_srv_conf_t { const char *name; @@ -63,21 +89,28 @@ typedef struct md_srv_conf_t { 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 renew_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 */ + struct md_pkeys_spec_t *pks; /* specification for private keys */ + md_timeslice_t *renew_window; /* time before expiration that starts renewal */ + md_timeslice_t *warn_window; /* time before expiration that warning are sent out */ - const char *ca_url; /* url of CA certificate service */ + struct apr_array_header_t *ca_urls; /* urls of CAs */ + const char *ca_contact; /* contact email registered to account */ 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 */ + const char *ca_eab_kid; /* != NULL, external account binding keyid */ + const char *ca_eab_hmac; /* != NULL, external account binding hmac */ + + int stapling; /* OCSP stapling enabled */ + int staple_others; /* Provide OCSP stapling for non-MD certificates */ + + const char *dns01_cmd; /* DNS challenge command, override global command */ md_t *current; /* md currently defined in <MDomainSet xxx> section */ - md_t *assigned; /* post_config: MD that applies to this server or NULL */ + struct apr_array_header_t *assigned; /* post_config: MDs that apply to this server */ + int is_ssl; /* SSLEngine is enabled here */ } md_srv_conf_t; void *md_config_create_svr(apr_pool_t *pool, server_rec *s); @@ -97,6 +130,9 @@ 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); + +void md_config_get_timespan(md_timeslice_t **pspan, const md_srv_conf_t *sc, md_config_var_t var); + +const md_t *md_get_for_domain(server_rec *s, const char *domain); #endif /* md_config_h */ diff --git a/modules/md/mod_md_drive.c b/modules/md/mod_md_drive.c new file mode 100644 index 0000000..5565f44 --- /dev/null +++ b/modules/md/mod_md_drive.c @@ -0,0 +1,345 @@ +/* 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_hash.h> +#include <apr_strings.h> +#include <apr_date.h> + +#include <httpd.h> +#include <http_core.h> +#include <http_protocol.h> +#include <http_request.h> +#include <http_log.h> + +#include "mod_watchdog.h" + +#include "md.h" +#include "md_curl.h" +#include "md_crypt.h" +#include "md_event.h" +#include "md_http.h" +#include "md_json.h" +#include "md_status.h" +#include "md_store.h" +#include "md_store_fs.h" +#include "md_log.h" +#include "md_result.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_private.h" +#include "mod_md_config.h" +#include "mod_md_status.h" +#include "mod_md_drive.h" + +/**************************************************************************************************/ +/* watchdog based impl. */ + +#define MD_RENEW_WATCHDOG_NAME "_md_renew_" + +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; + +struct md_renew_ctx_t { + apr_pool_t *p; + server_rec *s; + md_mod_conf_t *mc; + ap_watchdog_t *watchdog; + + apr_array_header_t *jobs; +}; + +static void process_drive_job(md_renew_ctx_t *dctx, md_job_t *job, apr_pool_t *ptemp) +{ + const md_t *md; + md_result_t *result = NULL; + apr_status_t rv; + + md_job_load(job); + /* Evaluate again on loaded value. Values will change when watchdog switches child process */ + if (apr_time_now() < job->next_run) return; + + job->next_run = 0; + if (job->finished && job->notified_renewed) { + /* finished and notification handled, nothing to do. */ + goto leave; + } + + md = md_get_by_name(dctx->mc->mds, job->mdomain); + AP_DEBUG_ASSERT(md); + + result = md_result_md_make(ptemp, md->name); + if (job->last_result) md_result_assign(result, job->last_result); + + if (md->state == MD_S_MISSING_INFORMATION) { + /* Missing information, this will not change until configuration + * is changed and server reloaded. */ + job->fatal_error = 1; + job->next_run = 0; + goto leave; + } + + if (md_will_renew_cert(md)) { + /* Renew the MDs credentials in a STAGING area. Might be invoked repeatedly + * without discarding previous/intermediate results. + * Only returns SUCCESS when the renewal is complete, e.g. STAGING has a + * complete set of new credentials. + */ + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10052) + "md(%s): state=%d, driving", job->mdomain, md->state); + + if (!md_reg_should_renew(dctx->mc->reg, md, dctx->p)) { + ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10053) + "md(%s): no need to renew", job->mdomain); + goto expiry; + } + + /* The (possibly configured) event handler may veto renewals. This + * is used in cluster installtations, see #233. */ + rv = md_event_raise("renewing", md->name, job, result, ptemp); + if (APR_SUCCESS != rv) { + ap_log_error(APLOG_MARK, APLOG_INFO, 0, dctx->s, APLOGNO(10060) + "%s: event-handler for 'renewing' returned %d, preventing renewal to proceed.", + job->mdomain, rv); + goto leave; + } + + md_job_start_run(job, result, md_reg_store_get(dctx->mc->reg)); + md_reg_renew(dctx->mc->reg, md, dctx->mc->env, 0, job->error_runs, result, ptemp); + md_job_end_run(job, result); + + if (APR_SUCCESS == result->status) { + /* Finished jobs might take a while before the results become valid. + * If that is in the future, request to run then */ + if (apr_time_now() < result->ready_at) { + md_job_retry_at(job, result->ready_at); + goto leave; + } + + if (!job->notified_renewed) { + md_job_save(job, result, ptemp); + md_job_notify(job, "renewed", result); + } + } + else { + ap_log_error( APLOG_MARK, APLOG_ERR, result->status, dctx->s, APLOGNO(10056) + "processing %s: %s", job->mdomain, result->detail); + md_job_log_append(job, "renewal-error", result->problem, result->detail); + md_event_holler("errored", job->mdomain, job, result, ptemp); + ap_log_error(APLOG_MARK, APLOG_INFO, 0, dctx->s, APLOGNO(10057) + "%s: encountered error for the %d. time, next run in %s", + job->mdomain, job->error_runs, + md_duration_print(ptemp, job->next_run - apr_time_now())); + } + } + +expiry: + if (!job->finished && md_reg_should_warn(dctx->mc->reg, md, dctx->p)) { + ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, dctx->s, + "md(%s): warn about expiration", md->name); + md_job_start_run(job, result, md_reg_store_get(dctx->mc->reg)); + md_job_notify(job, "expiring", result); + md_job_end_run(job, result); + } + +leave: + if (job->dirty && result) { + rv = md_job_save(job, result, ptemp); + ap_log_error(APLOG_MARK, APLOG_TRACE1, rv, dctx->s, "%s: saving job props", job->mdomain); + } +} + +int md_will_renew_cert(const md_t *md) +{ + if (md->renew_mode == MD_RENEW_MANUAL) { + return 0; + } + else if (md->renew_mode == MD_RENEW_AUTO && md->cert_files && md->cert_files->nelts) { + return 0; + } + return 1; +} + +static apr_time_t next_run_default(void) +{ + /* we'd like to run at least twice a day by default */ + return apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY / 2); +} + +static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp) +{ + md_renew_ctx_t *dctx = baton; + md_job_t *job; + apr_time_t next_run, wait_time; + int i; + + /* mod_watchdog invoked us as a single thread inside the whole server (on this machine). + * This might be a repeated run inside the same child (mod_watchdog keeps affinity as + * long as the child lives) or another/new child. + */ + switch (state) { + case AP_WATCHDOG_STATE_STARTING: + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10054) + "md watchdog start, auto drive %d mds", dctx->jobs->nelts); + break; + + case AP_WATCHDOG_STATE_RUNNING: + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10055) + "md watchdog run, auto drive %d mds", dctx->jobs->nelts); + + /* Process all drive jobs. They will update their next_run property + * and we schedule ourself at the earliest of all. A job may specify 0 + * as next_run to indicate that it wants to participate in the normal + * regular runs. */ + next_run = next_run_default(); + for (i = 0; i < dctx->jobs->nelts; ++i) { + job = APR_ARRAY_IDX(dctx->jobs, i, md_job_t *); + + if (apr_time_now() >= job->next_run) { + process_drive_job(dctx, job, ptemp); + } + + if (job->next_run && job->next_run < next_run) { + next_run = job->next_run; + } + } + + wait_time = next_run - apr_time_now(); + if (APLOGdebug(dctx->s)) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10107) + "next run in %s", md_duration_print(ptemp, wait_time)); + } + wd_set_interval(dctx->watchdog, wait_time, dctx, run_watchdog); + break; + + case AP_WATCHDOG_STATE_STOPPING: + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10058) + "md watchdog stopping"); + break; + } + + return APR_SUCCESS; +} + +apr_status_t md_renew_start_watching(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) +{ + apr_allocator_t *allocator; + md_renew_ctx_t *dctx; + apr_pool_t *dctxp; + apr_status_t rv; + md_t *md; + md_job_t *job; + int i; + + /* We use mod_watchdog to run a single thread in one of the child processes + * to monitor the MDs marked as watched, using the const data in the list + * mc->mds of our MD structures. + * + * The data in mc cannot be changed, as we may spawn copies in new child processes + * of the original data at any time. The child which hosts the watchdog thread + * may also die or be recycled, which causes a new watchdog thread to run + * in another process with the original data. + * + * Instead, we use our store to persist changes in group STAGING. This is + * kept writable to child processes, but the data stored there is not live. + * However, mod_watchdog makes sure that we only ever have a single thread in + * our server (on this machine) that writes there. Other processes, e.g. informing + * the user about progress, only read from there. + * + * All changes during driving an MD are stored as files in MG_SG_STAGING/<MD.name>. + * All will have "md.json" and "job.json". There may be a range of other files used + * by the protocol obtaining the certificate/keys. + * + * + */ + 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. + * Since we'll run in a single watchdog thread, using our own allocator will prevent + * any confusion in the parent pool. */ + apr_allocator_create(&allocator); + apr_allocator_max_free_set(allocator, 1); + rv = apr_pool_create_ex(&dctxp, p, NULL, allocator); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10062) "md_renew_watchdog: create pool"); + return rv; + } + apr_allocator_owner_set(allocator, dctxp); + apr_pool_tag(dctxp, "md_renew_watchdog"); + + dctx = apr_pcalloc(dctxp, sizeof(*dctx)); + dctx->p = dctxp; + dctx->s = s; + dctx->mc = mc; + + dctx->jobs = apr_array_make(dctx->p, mc->mds->nelts, sizeof(md_job_t *)); + for (i = 0; i < mc->mds->nelts; ++i) { + md = APR_ARRAY_IDX(mc->mds, i, md_t*); + if (!md || !md->watched) continue; + + job = md_reg_job_make(mc->reg, md->name, p); + APR_ARRAY_PUSH(dctx->jobs, md_job_t*) = job; + ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, dctx->s, + "md(%s): state=%d, created drive job", md->name, md->state); + + md_job_load(job); + if (job->error_runs) { + /* Server has just restarted. If we encounter an MD job with errors + * on a previous driving, we purge its STAGING area. + * This will reset the driving for the MD. It may run into the same + * error again, or in case of race/confusion/our error/CA error, it + * might allow the MD to succeed by a fresh start. + */ + ap_log_error( APLOG_MARK, APLOG_NOTICE, 0, dctx->s, APLOGNO(10064) + "md(%s): previous drive job showed %d errors, purging STAGING " + "area to reset.", md->name, job->error_runs); + md_store_purge(md_reg_store_get(dctx->mc->reg), p, MD_SG_STAGING, md->name); + md_store_purge(md_reg_store_get(dctx->mc->reg), p, MD_SG_CHALLENGES, md->name); + job->error_runs = 0; + } + } + + if (!dctx->jobs->nelts) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10065) + "no managed domain to drive, no watchdog needed."); + apr_pool_destroy(dctx->p); + return APR_SUCCESS; + } + + if (APR_SUCCESS != (rv = wd_get_instance(&dctx->watchdog, MD_RENEW_WATCHDOG_NAME, 0, 1, dctx->p))) { + ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, APLOGNO(10066) + "create md renew watchdog(%s)", MD_RENEW_WATCHDOG_NAME); + return rv; + } + rv = wd_register_callback(dctx->watchdog, 0, dctx, run_watchdog); + ap_log_error(APLOG_MARK, rv? APLOG_CRIT : APLOG_DEBUG, rv, s, APLOGNO(10067) + "register md renew watchdog(%s)", MD_RENEW_WATCHDOG_NAME); + return rv; +} diff --git a/modules/md/mod_md_drive.h b/modules/md/mod_md_drive.h new file mode 100644 index 0000000..40d6d67 --- /dev/null +++ b/modules/md/mod_md_drive.h @@ -0,0 +1,35 @@ +/* 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_drive_h +#define mod_md_md_drive_h + +struct md_mod_conf_t; +struct md_reg_t; + +typedef struct md_renew_ctx_t md_renew_ctx_t; + +int md_will_renew_cert(const md_t *md); + +/** + * Start driving the certificate renewal for MDs marked with watched. + */ +apr_status_t md_renew_start_watching(struct md_mod_conf_t *mc, server_rec *s, apr_pool_t *p); + + + + +#endif /* mod_md_md_drive_h */ diff --git a/modules/md/mod_md_ocsp.c b/modules/md/mod_md_ocsp.c new file mode 100644 index 0000000..1d1e282 --- /dev/null +++ b/modules/md/mod_md_ocsp.c @@ -0,0 +1,272 @@ +/* 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_time.h> +#include <apr_date.h> +#include <apr_strings.h> + +#include <httpd.h> +#include <http_core.h> +#include <http_log.h> +#include <http_ssl.h> + +#include "mod_watchdog.h" + +#include "md.h" +#include "md_crypt.h" +#include "md_http.h" +#include "md_json.h" +#include "md_ocsp.h" +#include "md_store.h" +#include "md_log.h" +#include "md_reg.h" +#include "md_time.h" +#include "md_util.h" + +#include "mod_md.h" +#include "mod_md_config.h" +#include "mod_md_private.h" +#include "mod_md_ocsp.h" + +static int staple_here(md_srv_conf_t *sc) +{ + if (!sc || !sc->mc->ocsp) return 0; + if (sc->assigned + && sc->assigned->nelts == 1 + && APR_ARRAY_IDX(sc->assigned, 0, const md_t*)->stapling) return 1; + return (md_config_geti(sc, MD_CONFIG_STAPLING) + && md_config_geti(sc, MD_CONFIG_STAPLE_OTHERS)); +} + +int md_ocsp_prime_status(server_rec *s, apr_pool_t *p, + const char *id, apr_size_t id_len, const char *pem) +{ + md_srv_conf_t *sc; + const md_t *md; + apr_array_header_t *chain; + apr_status_t rv = APR_ENOENT; + + sc = md_config_get(s); + if (!staple_here(sc)) goto cleanup; + + md = ((sc->assigned && sc->assigned->nelts == 1)? + APR_ARRAY_IDX(sc->assigned, 0, const md_t*) : NULL); + chain = apr_array_make(p, 5, sizeof(md_cert_t*)); + rv = md_cert_read_chain(chain, p, pem, strlen(pem)); + if (APR_SUCCESS != rv) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10268) "init stapling for: %s, " + "unable to parse PEM data", md? md->name : s->server_hostname); + goto cleanup; + } + else if (chain->nelts < 2) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10269) "init stapling for: %s, " + "need at least 2 certificates in PEM data", md? md->name : s->server_hostname); + rv = APR_EINVAL; + goto cleanup; + } + + rv = md_ocsp_prime(sc->mc->ocsp, id, id_len, + APR_ARRAY_IDX(chain, 0, md_cert_t*), + APR_ARRAY_IDX(chain, 1, md_cert_t*), md); + ap_log_error(APLOG_MARK, APLOG_TRACE1, rv, s, "init stapling for: %s", + md? md->name : s->server_hostname); + +cleanup: + return (APR_SUCCESS == rv)? OK : DECLINED; +} + +typedef struct { + unsigned char *der; + apr_size_t der_len; +} ocsp_copy_ctx_t; + +int md_ocsp_provide_status(server_rec *s, conn_rec *c, + const char *id, apr_size_t id_len, + ap_ssl_ocsp_copy_resp *cb, void *userdata) +{ + md_srv_conf_t *sc; + const md_t *md; + apr_status_t rv; + + sc = md_config_get(s); + if (!staple_here(sc)) goto declined; + + md = ((sc->assigned && sc->assigned->nelts == 1)? + APR_ARRAY_IDX(sc->assigned, 0, const md_t*) : NULL); + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, "get stapling for: %s", + md? md->name : s->server_hostname); + + rv = md_ocsp_get_status(cb, userdata, sc->mc->ocsp, id, id_len, c->pool, md); + if (APR_STATUS_IS_ENOENT(rv)) goto declined; + return OK; + +declined: + return DECLINED; +} + + +/**************************************************************************************************/ +/* watchdog based impl. */ + +#define MD_OCSP_WATCHDOG_NAME "_md_ocsp_" + +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_ocsp_ctx_t md_ocsp_ctx_t; + +struct md_ocsp_ctx_t { + apr_pool_t *p; + server_rec *s; + md_mod_conf_t *mc; + ap_watchdog_t *watchdog; +}; + +static apr_time_t next_run_default(void) +{ + /* we'd like to run at least hourly */ + return apr_time_now() + apr_time_from_sec(MD_SECS_PER_HOUR); +} + +static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp) +{ + md_ocsp_ctx_t *octx = baton; + apr_time_t next_run, wait_time; + + /* mod_watchdog invoked us as a single thread inside the whole server (on this machine). + * This might be a repeated run inside the same child (mod_watchdog keeps affinity as + * long as the child lives) or another/new child. + */ + switch (state) { + case AP_WATCHDOG_STATE_STARTING: + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, octx->s, APLOGNO(10197) + "md ocsp watchdog start, ocsp stapling %d certificates", + (int)md_ocsp_count(octx->mc->ocsp)); + break; + + case AP_WATCHDOG_STATE_RUNNING: + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, octx->s, APLOGNO(10198) + "md ocsp watchdog run, ocsp stapling %d certificates", + (int)md_ocsp_count(octx->mc->ocsp)); + + /* Process all drive jobs. They will update their next_run property + * and we schedule ourself at the earliest of all. A job may specify 0 + * as next_run to indicate that it wants to participate in the normal + * regular runs. */ + next_run = next_run_default(); + + md_ocsp_renew(octx->mc->ocsp, octx->p, ptemp, &next_run); + + wait_time = next_run - apr_time_now(); + if (APLOGdebug(octx->s)) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, octx->s, APLOGNO(10199) + "md ocsp watchdog next run in %s", + md_duration_print(ptemp, wait_time)); + } + wd_set_interval(octx->watchdog, wait_time, octx, run_watchdog); + break; + + case AP_WATCHDOG_STATE_STOPPING: + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, octx->s, APLOGNO(10200) + "md ocsp watchdog stopping"); + break; + } + + return APR_SUCCESS; +} + +static apr_status_t ocsp_remove_old_responses(md_mod_conf_t *mc, apr_pool_t *p) +{ + md_timeperiod_t keep_norm, keep; + + keep_norm.end = apr_time_now(); + keep_norm.start = keep_norm.end - MD_TIME_OCSP_KEEP_NORM; + keep = md_timeperiod_slice_before_end(&keep_norm, mc->ocsp_keep_window); + /* remove any ocsp response older than keep.start */ + return md_ocsp_remove_responses_older_than(mc->ocsp, p, keep.start); +} + +apr_status_t md_ocsp_start_watching(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) +{ + apr_allocator_t *allocator; + md_ocsp_ctx_t *octx; + apr_pool_t *octxp; + apr_status_t rv; + + 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(10201) + "mod_watchdog is required for OCSP stapling"); + return APR_EGENERAL; + } + + /* We want our own pool with own allocator to keep data across watchdog invocations. + * Since we'll run in a single watchdog thread, using our own allocator will prevent + * any confusion in the parent pool. */ + apr_allocator_create(&allocator); + apr_allocator_max_free_set(allocator, 1); + rv = apr_pool_create_ex(&octxp, p, NULL, allocator); + if (rv != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10205) "md_ocsp_watchdog: create pool"); + return rv; + } + apr_allocator_owner_set(allocator, octxp); + apr_pool_tag(octxp, "md_ocsp_watchdog"); + + octx = apr_pcalloc(octxp, sizeof(*octx)); + octx->p = octxp; + octx->s = s; + octx->mc = mc; + + /* Time for some house keeping, before the server goes live (again): + * - we store OCSP responses for each certificate individually by its SHA-1 id + * - this means, as long as certificate do not change, the number of response + * files remains stable. + * - But when a certificate changes (is replaced), the response is obsolete + * - we do not get notified when a certificate is no longer used. An admin + * might just reconfigure or change the content of a file (backup/restore etc.) + * - also, certificates might be added by some openssl config commands or other + * modules that we do not immediately see right at startup. We cannot assume + * that any OCSP response we cannot relate to a certificate RIGHT NOW, is no + * longer needed. + * - since the response files are relatively small, we have no problem with + * keeping them around for a while. We just do not want an ever growing store. + * - The simplest and effective way seems to be to just remove files older + * a certain amount of time. Take a 7 day default and let the admin configure + * it for very special setups. + */ + ocsp_remove_old_responses(mc, octx->p); + + rv = wd_get_instance(&octx->watchdog, MD_OCSP_WATCHDOG_NAME, 0, 1, octx->p); + if (APR_SUCCESS != rv) { + ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, APLOGNO(10202) + "create md ocsp watchdog(%s)", MD_OCSP_WATCHDOG_NAME); + return rv; + } + rv = wd_register_callback(octx->watchdog, 0, octx, run_watchdog); + ap_log_error(APLOG_MARK, rv? APLOG_CRIT : APLOG_DEBUG, rv, s, APLOGNO(10203) + "register md ocsp watchdog(%s)", MD_OCSP_WATCHDOG_NAME); + return rv; +} + + + diff --git a/modules/md/mod_md_ocsp.h b/modules/md/mod_md_ocsp.h new file mode 100644 index 0000000..a3f9502 --- /dev/null +++ b/modules/md/mod_md_ocsp.h @@ -0,0 +1,33 @@ +/* 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_ocsp_h +#define mod_md_md_ocsp_h + + +int md_ocsp_prime_status(server_rec *s, apr_pool_t *p, + const char *id, apr_size_t id_len, const char *pem); + +int md_ocsp_provide_status(server_rec *s, conn_rec *c, const char *id, apr_size_t id_len, + ap_ssl_ocsp_copy_resp *cb, void *userdata); + +/** + * Start watchdog for retrieving/updating ocsp status. + */ +apr_status_t md_ocsp_start_watching(struct md_mod_conf_t *mc, server_rec *s, apr_pool_t *p); + + +#endif /* mod_md_md_ocsp_h */ diff --git a/modules/md/mod_md_os.c b/modules/md/mod_md_os.c index f96d566..06a5bee 100644 --- a/modules/md/mod_md_os.c +++ b/modules/md/mod_md_os.c @@ -17,10 +17,6 @@ #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> @@ -29,9 +25,6 @@ #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 @@ -41,14 +34,20 @@ 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); +#if AP_NEED_SET_MUTEX_PERMS && HAVE_UNISTD_H + /* Since we only switch user when running as root, we only need to chown directories + * in that case. Otherwise, the server will ignore any "user/group" directives and + * child processes have the same privileges as the parent. + */ + if (!geteuid()) { + 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 rv; } return APR_SUCCESS; #else @@ -58,10 +57,10 @@ apr_status_t md_try_chown(const char *fname, unsigned int uid, int gid, apr_pool 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 +#ifdef WIN32 return APR_ENOTIMPL; +#else + return md_try_chown(fname, ap_unixd_config.user_id, -1, p); #endif } diff --git a/modules/md/mod_md_status.c b/modules/md/mod_md_status.c new file mode 100644 index 0000000..6b29256 --- /dev/null +++ b/modules/md/mod_md_status.c @@ -0,0 +1,987 @@ +/* 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_time.h> +#include <apr_date.h> +#include <apr_strings.h> + +#include <httpd.h> +#include <http_core.h> +#include <http_protocol.h> +#include <http_request.h> +#include <http_log.h> + +#include "mod_status.h" + +#include "md.h" +#include "md_curl.h" +#include "md_crypt.h" +#include "md_http.h" +#include "md_ocsp.h" +#include "md_json.h" +#include "md_status.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_private.h" +#include "mod_md_config.h" +#include "mod_md_drive.h" +#include "mod_md_status.h" + +/**************************************************************************************************/ +/* Certificate status */ + +#define APACHE_PREFIX "/.httpd/" +#define MD_STATUS_RESOURCE APACHE_PREFIX"certificate-status" +#define HTML_STATUS(X) (!((X)->flags & AP_STATUS_SHORT)) + +int md_http_cert_status(request_rec *r) +{ + int i; + md_json_t *resp, *mdj, *cj; + const md_srv_conf_t *sc; + const md_t *md; + md_pkey_spec_t *spec; + const char *keyname; + apr_bucket_brigade *bb; + apr_status_t rv; + + if (!r->parsed_uri.path || strcmp(MD_STATUS_RESOURCE, r->parsed_uri.path)) + return DECLINED; + + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, + "requesting status for: %s", r->hostname); + + /* We are looking for information about a staged certificate */ + sc = ap_get_module_config(r->server->module_config, &md_module); + if (!sc || !sc->mc || !sc->mc->reg || !sc->mc->certificate_status_enabled) return DECLINED; + md = md_get_by_domain(sc->mc->mds, r->hostname); + if (!md) return DECLINED; + + if (r->method_number != M_GET) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, + "md(%s): status supports only GET", md->name); + return HTTP_NOT_IMPLEMENTED; + } + + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, + "requesting status for MD: %s", md->name); + + rv = md_status_get_md_json(&mdj, md, sc->mc->reg, sc->mc->ocsp, r->pool); + if (APR_SUCCESS != rv) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(10204) + "loading md status for %s", md->name); + return HTTP_INTERNAL_SERVER_ERROR; + } + + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, + "status for MD: %s is %s", md->name, md_json_writep(mdj, r->pool, MD_JSON_FMT_INDENT)); + + resp = md_json_create(r->pool); + + if (md_json_has_key(mdj, MD_KEY_CERT, MD_KEY_VALID, NULL)) { + md_json_setj(md_json_getj(mdj, MD_KEY_CERT, MD_KEY_VALID, NULL), resp, MD_KEY_VALID, NULL); + } + + for (i = 0; i < md_cert_count(md); ++i) { + spec = md_pkeys_spec_get(md->pks, i); + keyname = md_pkey_spec_name(spec); + cj = md_json_create(r->pool); + + if (md_json_has_key(mdj, MD_KEY_CERT, keyname, MD_KEY_VALID, NULL)) { + md_json_setj(md_json_getj(mdj, MD_KEY_CERT, keyname, MD_KEY_VALID, NULL), + cj, MD_KEY_VALID, NULL); + } + + if (md_json_has_key(mdj, MD_KEY_CERT, keyname, MD_KEY_SERIAL, NULL)) { + md_json_sets(md_json_gets(mdj, MD_KEY_CERT, keyname, MD_KEY_SERIAL, NULL), + cj, MD_KEY_SERIAL, NULL); + } + if (md_json_has_key(mdj, MD_KEY_CERT, keyname, MD_KEY_SHA256_FINGERPRINT, NULL)) { + md_json_sets(md_json_gets(mdj, MD_KEY_CERT, keyname, MD_KEY_SHA256_FINGERPRINT, NULL), + cj, MD_KEY_SHA256_FINGERPRINT, NULL); + } + md_json_setj(cj, resp, keyname, NULL ); + } + + if (md_json_has_key(mdj, MD_KEY_RENEWAL, NULL)) { + /* copy over the information we want to make public about this: + * - when not finished, add an empty object to indicate something is going on + * - when a certificate is staged, add the information from that */ + cj = md_json_getj(mdj, MD_KEY_RENEWAL, MD_KEY_CERT, NULL); + cj = cj? cj : md_json_create(r->pool); + md_json_setj(cj, resp, MD_KEY_RENEWAL, MD_KEY_CERT, NULL); + } + + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "md[%s]: sending status", md->name); + apr_table_set(r->headers_out, "Content-Type", "application/json"); + bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + md_json_writeb(resp, MD_JSON_FMT_INDENT, bb); + ap_pass_brigade(r->output_filters, bb); + apr_brigade_cleanup(bb); + + return DONE; +} + +/**************************************************************************************************/ +/* Status hook */ + +typedef struct { + apr_pool_t *p; + const md_mod_conf_t *mc; + apr_bucket_brigade *bb; + int flags; + const char *prefix; + const char *separator; +} status_ctx; + +typedef struct status_info status_info; + +static void add_json_val(status_ctx *ctx, md_json_t *j); + +typedef void add_status_fn(status_ctx *ctx, md_json_t *mdj, const status_info *info); + +struct status_info { + const char *label; + const char *key; + add_status_fn *fn; +}; + +static void si_val_status(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + const char *s = "unknown"; + apr_time_t until; + (void)info; + switch (md_json_getl(mdj, info->key, NULL)) { + case MD_S_INCOMPLETE: + s = md_json_gets(mdj, MD_KEY_STATE_DESCR, NULL); + s = s? apr_psprintf(ctx->p, "incomplete: %s", s) : "incomplete"; + break; + case MD_S_EXPIRED_DEPRECATED: + case MD_S_COMPLETE: + until = md_json_get_time(mdj, MD_KEY_CERT, MD_KEY_VALID, MD_KEY_UNTIL, NULL); + s = (!until || until > apr_time_now())? "good" : "expired"; + break; + case MD_S_ERROR: s = "error"; break; + case MD_S_MISSING_INFORMATION: s = "missing information"; break; + default: break; + } + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, s); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%s: %s\n", + ctx->prefix, info->label, s); + } +} + +static void si_val_url(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + const char *url, *s; + + s = url = md_json_gets(mdj, info->key, NULL); + if (!url) return; + s = md_get_ca_name_from_url(ctx->p, url); + if (HTML_STATUS(ctx)) { + apr_brigade_printf(ctx->bb, NULL, NULL, "<a href='%s'>%s</a>", + ap_escape_html2(ctx->p, url, 1), + ap_escape_html2(ctx->p, s, 1)); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName: %s\n", + ctx->prefix, info->label, s); + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL: %s\n", + ctx->prefix, info->label, url); + } +} + +static void print_date(status_ctx *ctx, apr_time_t timestamp, const char *title) +{ + apr_bucket_brigade *bb = ctx->bb; + if (timestamp > 0) { + char ts[128]; + char ts2[128]; + apr_time_exp_t texp; + apr_size_t len; + + apr_time_exp_gmt(&texp, timestamp); + apr_strftime(ts, &len, sizeof(ts2)-1, "%Y-%m-%d", &texp); + ts[len] = '\0'; + if (!title) { + apr_strftime(ts2, &len, sizeof(ts)-1, "%Y-%m-%dT%H:%M:%SZ", &texp); + ts2[len] = '\0'; + title = ts2; + } + if (HTML_STATUS(ctx)) { + apr_brigade_printf(bb, NULL, NULL, + "<span title='%s' style='white-space: nowrap;'>%s</span>", + ap_escape_html2(bb->p, title, 1), ts); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%s%s: %s\n", + ctx->prefix, title, ts); + } + } +} + +static void print_time(status_ctx *ctx, const char *label, apr_time_t t) +{ + apr_bucket_brigade *bb = ctx->bb; + apr_time_t now; + const char *pre, *post, *sep; + char ts[APR_RFC822_DATE_LEN]; + char ts2[128]; + apr_time_exp_t texp; + apr_size_t len; + apr_interval_time_t delta; + + if (t == 0) { + /* timestamp is 0, we use that for "not set" */ + return; + } + apr_time_exp_gmt(&texp, t); + now = apr_time_now(); + pre = post = ""; + sep = (label && strlen(label))? " " : ""; + delta = 0; + if (HTML_STATUS(ctx)) { + apr_rfc822_date(ts, t); + if (t > now) { + delta = t - now; + pre = "in "; + } + else { + delta = now - t; + post = " ago"; + } + if (delta >= (4 * apr_time_from_sec(MD_SECS_PER_DAY))) { + apr_strftime(ts2, &len, sizeof(ts2)-1, "%Y-%m-%d", &texp); + ts2[len] = '\0'; + apr_brigade_printf(bb, NULL, NULL, "%s%s<span title='%s' " + "style='white-space: nowrap;'>%s</span>", + label, sep, ts, ts2); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%s%s<span title='%s'>%s%s%s</span>", + label, sep, ts, pre, md_duration_roughly(bb->p, delta), post); + } + } + else { + delta = t - now; + apr_brigade_printf(bb, NULL, NULL, "%s%s: %" APR_TIME_T_FMT "\n", + ctx->prefix, label, apr_time_sec(delta)); + } +} + +static void si_val_valid_time(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + const char *sfrom, *suntil, *sep, *title; + apr_time_t from, until; + + sep = NULL; + sfrom = md_json_gets(mdj, info->key, MD_KEY_FROM, NULL); + from = sfrom? apr_date_parse_rfc(sfrom) : 0; + suntil = md_json_gets(mdj, info->key, MD_KEY_UNTIL, NULL); + until = suntil?apr_date_parse_rfc(suntil) : 0; + + if (HTML_STATUS(ctx)) { + if (from > apr_time_now()) { + apr_brigade_puts(ctx->bb, NULL, NULL, "from "); + print_date(ctx, from, sfrom); + sep = " "; + } + if (until) { + if (sep) apr_brigade_puts(ctx->bb, NULL, NULL, sep); + apr_brigade_puts(ctx->bb, NULL, NULL, "until "); + title = sfrom? apr_psprintf(ctx->p, "%s - %s", sfrom, suntil) : suntil; + print_date(ctx, until, title); + } + } + else { + if (from > apr_time_now()) { + print_date(ctx, from, + apr_pstrcat(ctx->p, info->label, "From", NULL)); + } + if (until) { + print_date(ctx, until, + apr_pstrcat(ctx->p, info->label, "Until", NULL)); + } + } +} + +static void si_add_header(status_ctx *ctx, const status_info *info) +{ + if (HTML_STATUS(ctx)) { + const char *html = ap_escape_html2(ctx->p, info->label, 1); + apr_brigade_printf(ctx->bb, NULL, NULL, "<th class=\"%s\">%s</th>", html, html); + } +} + +static void si_val_cert_valid_time(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + md_json_t *jcert; + status_info sub = *info; + + sub.key = MD_KEY_VALID; + jcert = md_json_getj(mdj, info->key, NULL); + if (jcert) si_val_valid_time(ctx, jcert, &sub); +} + +static void val_url_print(status_ctx *ctx, const status_info *info, + const char*url, const char *proto, int i) +{ + const char *s; + + if (proto && !strcmp(proto, "tailscale")) { + s = "tailscale"; + } + else if (url) { + s = md_get_ca_name_from_url(ctx->p, url); + } + else { + return; + } + if (HTML_STATUS(ctx)) { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s<a href='%s'>%s</a>", + i? " " : "", + ap_escape_html2(ctx->p, url, 1), + ap_escape_html2(ctx->p, s, 1)); + } + else if (i == 0) { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName: %s\n", + ctx->prefix, info->label, s); + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL: %s\n", + ctx->prefix, info->label, url); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName%d: %s\n", + ctx->prefix, info->label, i, s); + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL%d: %s\n", + ctx->prefix, info->label, i, url); + } +} + +static void si_val_ca_urls(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + md_json_t *jcert; + const char *proto, *url; + apr_array_header_t *urls; + int i; + + jcert = md_json_getj(mdj, info->key, NULL); + if (!jcert) { + return; + } + + proto = md_json_gets(jcert, MD_KEY_PROTO, NULL); + url = md_json_gets(jcert, MD_KEY_URL, NULL); + if (url) { + /* print the effective CA url used, if set */ + val_url_print(ctx, info, url, proto, 0); + } + else { + /* print the available CA urls configured */ + urls = apr_array_make(ctx->p, 3, sizeof(const char*)); + md_json_getsa(urls, jcert, MD_KEY_URLS, NULL); + for (i = 0; i < urls->nelts; ++i) { + url = APR_ARRAY_IDX(urls, i, const char*); + val_url_print(ctx, info, url, proto, i); + } + } +} + +static int count_certs(void *baton, const char *key, md_json_t *json) +{ + int *pcount = baton; + + (void)json; + if (strcmp(key, MD_KEY_VALID)) { + *pcount += 1; + } + return 1; +} + +static void print_job_summary(status_ctx *ctx, md_json_t *mdj, const char *key, + const char *separator) +{ + apr_bucket_brigade *bb = ctx->bb; + char buffer[HUGE_STRING_LEN]; + apr_status_t rv; + int finished, errors, cert_count; + apr_time_t t; + const char *s, *line; + + if (!md_json_has_key(mdj, key, NULL)) { + return; + } + + finished = md_json_getb(mdj, key, MD_KEY_FINISHED, NULL); + errors = (int)md_json_getl(mdj, key, MD_KEY_ERRORS, NULL); + rv = (apr_status_t)md_json_getl(mdj, key, MD_KEY_LAST, MD_KEY_STATUS, NULL); + + line = separator? separator : ""; + + if (rv != APR_SUCCESS) { + char *errstr = apr_strerror(rv, buffer, sizeof(buffer)); + s = md_json_gets(mdj, key, MD_KEY_LAST, MD_KEY_PROBLEM, NULL); + if (HTML_STATUS(ctx)) { + line = apr_psprintf(bb->p, "%s Error[%s]: %s", line, + errstr, s? s : ""); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%sLastStatus: %s\n", ctx->prefix, errstr); + apr_brigade_printf(bb, NULL, NULL, "%sLastProblem: %s\n", ctx->prefix, s); + } + } + + if (!HTML_STATUS(ctx)) { + apr_brigade_printf(bb, NULL, NULL, "%sFinished: %s\n", ctx->prefix, + finished ? "yes" : "no"); + } + if (finished) { + cert_count = 0; + md_json_iterkey(count_certs, &cert_count, mdj, key, MD_KEY_CERT, NULL); + if (HTML_STATUS(ctx)) { + if (cert_count > 0) { + line =apr_psprintf(bb->p, "%s finished, %d new certificate%s staged.", + line, cert_count, cert_count > 1? "s" : ""); + } + else { + line = apr_psprintf(bb->p, "%s finished successfully.", line); + } + } + else { + apr_brigade_printf(bb, NULL, NULL, "%sNewStaged: %d\n", ctx->prefix, cert_count); + } + } + else { + s = md_json_gets(mdj, key, MD_KEY_LAST, MD_KEY_DETAIL, NULL); + if (s) { + if (HTML_STATUS(ctx)) { + line = apr_psprintf(bb->p, "%s %s", line, s); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%sLastDetail: %s\n", ctx->prefix, s); + } + } + } + + errors = (int)md_json_getl(mdj, MD_KEY_ERRORS, NULL); + if (errors > 0) { + if (HTML_STATUS(ctx)) { + line = apr_psprintf(bb->p, "%s (%d retr%s) ", line, + errors, (errors > 1)? "y" : "ies"); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%sRetries: %d\n", ctx->prefix, errors); + } + } + + if (HTML_STATUS(ctx)) { + apr_brigade_puts(bb, NULL, NULL, line); + } + + t = md_json_get_time(mdj, key, MD_KEY_NEXT_RUN, NULL); + if (t > apr_time_now() && !finished) { + print_time(ctx, + HTML_STATUS(ctx) ? "\nNext run" : "NextRun", + t); + } + else if (line[0] != '\0') { + if (HTML_STATUS(ctx)) { + apr_brigade_puts(bb, NULL, NULL, "\nOngoing..."); + } + else { + apr_brigade_printf(bb, NULL, NULL, "%s: Ongoing\n", ctx->prefix); + } + } +} + +static void si_val_activity(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + apr_time_t t; + const char *prefix = ctx->prefix; + + (void)info; + if (!HTML_STATUS(ctx)) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL); + } + + if (md_json_has_key(mdj, MD_KEY_RENEWAL, NULL)) { + print_job_summary(ctx, mdj, MD_KEY_RENEWAL, NULL); + return; + } + + t = md_json_get_time(mdj, MD_KEY_RENEW_AT, NULL); + if (t > apr_time_now()) { + print_time(ctx, "Renew", t); + } + else if (t) { + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "Pending"); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s: %s", ctx->prefix, "Pending"); + } + } + else if (MD_RENEW_MANUAL == md_json_getl(mdj, MD_KEY_RENEW_MODE, NULL)) { + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "Manual renew"); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s: %s", ctx->prefix, "Manual renew"); + } + } + if (!HTML_STATUS(ctx)) { + ctx->prefix = prefix; + } +} + +static int cert_check_iter(void *baton, const char *key, md_json_t *json) +{ + status_ctx *ctx = baton; + const char *fingerprint; + + fingerprint = md_json_gets(json, MD_KEY_SHA256_FINGERPRINT, NULL); + if (fingerprint) { + if (HTML_STATUS(ctx)) { + apr_brigade_printf(ctx->bb, NULL, NULL, + "<a href=\"%s%s\">%s[%s]</a><br>", + ctx->mc->cert_check_url, fingerprint, + ctx->mc->cert_check_name, key); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, + "%sType: %s\n", + ctx->prefix, + key); + apr_brigade_printf(ctx->bb, NULL, NULL, + "%sName: %s\n", + ctx->prefix, + ctx->mc->cert_check_name); + apr_brigade_printf(ctx->bb, NULL, NULL, + "%sURL: %s%s\n", + ctx->prefix, + ctx->mc->cert_check_url, fingerprint); + apr_brigade_printf(ctx->bb, NULL, NULL, + "%sFingerprint: %s\n", + ctx->prefix, + fingerprint); + } + } + return 1; +} + +static void si_val_remote_check(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + (void)info; + if (ctx->mc->cert_check_name && ctx->mc->cert_check_url) { + const char *prefix = ctx->prefix; + if (!HTML_STATUS(ctx)) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL); + } + md_json_iterkey(cert_check_iter, ctx, mdj, MD_KEY_CERT, NULL); + if (!HTML_STATUS(ctx)) { + ctx->prefix = prefix; + } + } +} + +static void si_val_stapling(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + (void)info; + if (!md_json_getb(mdj, MD_KEY_STAPLING, NULL)) return; + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "on"); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s: on", ctx->prefix); + } +} + +static int json_iter_val(void *data, size_t index, md_json_t *json) +{ + status_ctx *ctx = data; + const char *prefix = ctx->prefix; + if (HTML_STATUS(ctx)) { + if (index) apr_brigade_puts(ctx->bb, NULL, NULL, ctx->separator); + } + else { + ctx->prefix = apr_pstrcat(ctx->p, prefix, apr_psprintf(ctx->p, "[%" APR_SIZE_T_FMT "]", index), NULL); + } + add_json_val(ctx, json); + if (!HTML_STATUS(ctx)) { + ctx->prefix = prefix; + } + return 1; +} + +static void add_json_val(status_ctx *ctx, md_json_t *j) +{ + if (!j) return; + if (md_json_is(MD_JSON_TYPE_ARRAY, j, NULL)) { + md_json_itera(json_iter_val, ctx, j, NULL); + return; + } + if (!HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, ctx->prefix); + apr_brigade_puts(ctx->bb, NULL, NULL, ": "); + } + if (md_json_is(MD_JSON_TYPE_INT, j, NULL)) { + md_json_writeb(j, MD_JSON_FMT_COMPACT, ctx->bb); + } + else if (md_json_is(MD_JSON_TYPE_STRING, j, NULL)) { + apr_brigade_puts(ctx->bb, NULL, NULL, md_json_gets(j, NULL)); + } + else if (md_json_is(MD_JSON_TYPE_OBJECT, j, NULL)) { + md_json_writeb(j, MD_JSON_FMT_COMPACT, ctx->bb); + } + else if (md_json_is(MD_JSON_TYPE_BOOL, j, NULL)) { + apr_brigade_puts(ctx->bb, NULL, NULL, md_json_getb(j, NULL)? "on" : "off"); + } + if (!HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "\n"); + } +} + +static void si_val_names(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + const char *prefix = ctx->prefix; + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "<div style=\"max-width:400px;\">"); + } + else { + ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL); + } + add_json_val(ctx, md_json_getj(mdj, info->key, NULL)); + if (HTML_STATUS(ctx)) { + apr_brigade_puts(ctx->bb, NULL, NULL, "</div>"); + } + else { + ctx->prefix = prefix; + } +} + +static void add_status_cell(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + if (info->fn) { + info->fn(ctx, mdj, info); + } + else { + const char *prefix = ctx->prefix; + if (!HTML_STATUS(ctx)) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL); + } + add_json_val(ctx, md_json_getj(mdj, info->key, NULL)); + if (!HTML_STATUS(ctx)) { + ctx->prefix = prefix; + } + } +} + +static const status_info status_infos[] = { + { "Domain", MD_KEY_NAME, NULL }, + { "Names", MD_KEY_DOMAINS, si_val_names }, + { "Status", MD_KEY_STATE, si_val_status }, + { "Valid", MD_KEY_CERT, si_val_cert_valid_time }, + { "CA", MD_KEY_CA, si_val_ca_urls }, + { "Stapling", MD_KEY_STAPLING, si_val_stapling }, + { "CheckAt", MD_KEY_SHA256_FINGERPRINT, si_val_remote_check }, + { "Activity", MD_KEY_NOTIFIED, si_val_activity }, +}; + +static int add_md_row(void *baton, apr_size_t index, md_json_t *mdj) +{ + status_ctx *ctx = baton; + const char *prefix = ctx->prefix; + int i; + + if (HTML_STATUS(ctx)) { + apr_brigade_printf(ctx->bb, NULL, NULL, "<tr class=\"%s\">", (index % 2)? "odd" : "even"); + for (i = 0; i < (int)(sizeof(status_infos)/sizeof(status_infos[0])); ++i) { + apr_brigade_puts(ctx->bb, NULL, NULL, "<td>"); + add_status_cell(ctx, mdj, &status_infos[i]); + apr_brigade_puts(ctx->bb, NULL, NULL, "</td>"); + } + apr_brigade_puts(ctx->bb, NULL, NULL, "</tr>"); + } else { + for (i = 0; i < (int)(sizeof(status_infos)/sizeof(status_infos[0])); ++i) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, apr_psprintf(ctx->p, "[%" APR_SIZE_T_FMT "]", index), NULL); + add_status_cell(ctx, mdj, &status_infos[i]); + ctx->prefix = prefix; + } + } + return 1; +} + +static int md_name_cmp(const void *v1, const void *v2) +{ + return strcmp((*(const md_t**)v1)->name, (*(const md_t**)v2)->name); +} + +int md_domains_status_hook(request_rec *r, int flags) +{ + const md_srv_conf_t *sc; + const md_mod_conf_t *mc; + int i; + status_ctx ctx; + apr_array_header_t *mds; + md_json_t *jstatus, *jstock; + + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for managed domains, start"); + sc = ap_get_module_config(r->server->module_config, &md_module); + if (!sc) return DECLINED; + mc = sc->mc; + if (!mc || !mc->server_status_enabled) return DECLINED; + + ctx.p = r->pool; + ctx.mc = mc; + ctx.bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + ctx.flags = flags; + ctx.prefix = "ManagedCertificates"; + ctx.separator = " "; + + mds = apr_array_copy(r->pool, mc->mds); + qsort(mds->elts, (size_t)mds->nelts, sizeof(md_t *), md_name_cmp); + + if (!HTML_STATUS(&ctx)) { + int total = 0, complete = 0, renewing = 0, errored = 0, ready = 0; + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "no-html managed domain status summary"); + if (mc->mds->nelts > 0) { + md_status_take_stock(&jstock, mds, mc->reg, r->pool); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON managed domain status summary"); + total = (int)md_json_getl(jstock, MD_KEY_TOTAL, NULL); + complete = (int)md_json_getl(jstock, MD_KEY_COMPLETE, NULL); + renewing = (int)md_json_getl(jstock, MD_KEY_RENEWING, NULL); + errored = (int)md_json_getl(jstock, MD_KEY_ERRORED, NULL); + ready = (int)md_json_getl(jstock, MD_KEY_READY, NULL); + } + apr_brigade_printf(ctx.bb, NULL, NULL, "%sTotal: %d\n", ctx.prefix, total); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sOK: %d\n", ctx.prefix, complete); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sRenew: %d\n", ctx.prefix, renewing); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sErrored: %d\n", ctx.prefix, errored); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sReady: %d\n", ctx.prefix, ready); + } + if (mc->mds->nelts > 0) { + md_status_get_json(&jstatus, mds, mc->reg, mc->ocsp, r->pool); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON managed domain status"); + if (HTML_STATUS(&ctx)) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "html managed domain status table"); + apr_brigade_puts(ctx.bb, NULL, NULL, + "<hr>\n<h3>Managed Certificates</h3>\n<table class='md_status'><thead><tr>\n"); + for (i = 0; i < (int)(sizeof(status_infos)/sizeof(status_infos[0])); ++i) { + si_add_header(&ctx, &status_infos[i]); + } + apr_brigade_puts(ctx.bb, NULL, NULL, "</tr>\n</thead><tbody>"); + } + else { + ctx.prefix = "ManagedDomain"; + } + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "iterating JSON managed domain status"); + md_json_itera(add_md_row, &ctx, jstatus, MD_KEY_MDS, NULL); + if (HTML_STATUS(&ctx)) { + apr_brigade_puts(ctx.bb, NULL, NULL, "</td></tr>\n</tbody>\n</table>\n"); + } + } + + ap_pass_brigade(r->output_filters, ctx.bb); + apr_brigade_cleanup(ctx.bb); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for managed domains, end"); + + return OK; +} + +static void si_val_ocsp_activity(status_ctx *ctx, md_json_t *mdj, const status_info *info) +{ + apr_time_t t; + const char *prefix = ctx->prefix; + + (void)info; + if (!HTML_STATUS(ctx)) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, info->label, NULL); + } + t = md_json_get_time(mdj, MD_KEY_RENEW_AT, NULL); + print_time(ctx, "Refresh", t); + print_job_summary(ctx, mdj, MD_KEY_RENEWAL, ": "); + if (!HTML_STATUS(ctx)) { + ctx->prefix = prefix; + } +} + +static const status_info ocsp_status_infos[] = { + { "Domain", MD_KEY_DOMAIN, NULL }, + { "CertificateID", MD_KEY_ID, NULL }, + { "OCSPStatus", MD_KEY_STATUS, NULL }, + { "StaplingValid", MD_KEY_VALID, si_val_valid_time }, + { "Responder", MD_KEY_URL, si_val_url }, + { "Activity", MD_KEY_NOTIFIED, si_val_ocsp_activity }, +}; + +static int add_ocsp_row(void *baton, apr_size_t index, md_json_t *mdj) +{ + status_ctx *ctx = baton; + const char *prefix = ctx->prefix; + int i; + + if (HTML_STATUS(ctx)) { + apr_brigade_printf(ctx->bb, NULL, NULL, "<tr class=\"%s\">", (index % 2)? "odd" : "even"); + for (i = 0; i < (int)(sizeof(ocsp_status_infos)/sizeof(ocsp_status_infos[0])); ++i) { + apr_brigade_puts(ctx->bb, NULL, NULL, "<td>"); + add_status_cell(ctx, mdj, &ocsp_status_infos[i]); + apr_brigade_puts(ctx->bb, NULL, NULL, "</td>"); + } + apr_brigade_puts(ctx->bb, NULL, NULL, "</tr>"); + } else { + for (i = 0; i < (int)(sizeof(ocsp_status_infos)/sizeof(ocsp_status_infos[0])); ++i) { + ctx->prefix = apr_pstrcat(ctx->p, prefix, apr_psprintf(ctx->p, "[%" APR_SIZE_T_FMT "]", index), NULL); + add_status_cell(ctx, mdj, &ocsp_status_infos[i]); + ctx->prefix = prefix; + } + } + return 1; +} + +int md_ocsp_status_hook(request_rec *r, int flags) +{ + const md_srv_conf_t *sc; + const md_mod_conf_t *mc; + int i; + status_ctx ctx; + md_json_t *jstatus, *jstock; + + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for ocsp stapling, start"); + sc = ap_get_module_config(r->server->module_config, &md_module); + if (!sc) return DECLINED; + mc = sc->mc; + if (!mc || !mc->server_status_enabled) return DECLINED; + + ctx.p = r->pool; + ctx.mc = mc; + ctx.bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + ctx.flags = flags; + ctx.prefix = "ManagedStaplings"; + ctx.separator = " "; + + if (!HTML_STATUS(&ctx)) { + int total = 0, good = 0, revoked = 0, unknown = 0; + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "no-html ocsp stapling status summary"); + if (md_ocsp_count(mc->ocsp) > 0) { + md_ocsp_get_summary(&jstock, mc->ocsp, r->pool); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON ocsp stapling status summary"); + total = (int)md_json_getl(jstock, MD_KEY_TOTAL, NULL); + good = (int)md_json_getl(jstock, MD_KEY_GOOD, NULL); + revoked = (int)md_json_getl(jstock, MD_KEY_REVOKED, NULL); + unknown = (int)md_json_getl(jstock, MD_KEY_UNKNOWN, NULL); + } + apr_brigade_printf(ctx.bb, NULL, NULL, "%sTotal: %d\n", ctx.prefix, total); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sOK: %d\n", ctx.prefix, good); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sRenew: %d\n", ctx.prefix, revoked); + apr_brigade_printf(ctx.bb, NULL, NULL, "%sErrored: %d\n", ctx.prefix, unknown); + } + if (md_ocsp_count(mc->ocsp) > 0) { + md_ocsp_get_status_all(&jstatus, mc->ocsp, r->pool); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "got JSON ocsp stapling status"); + if (HTML_STATUS(&ctx)) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "html ocsp stapling status table"); + apr_brigade_puts(ctx.bb, NULL, NULL, + "<hr>\n<h3>Managed Staplings</h3>\n<table class='md_ocsp_status'><thead><tr>\n"); + for (i = 0; i < (int)(sizeof(ocsp_status_infos)/sizeof(ocsp_status_infos[0])); ++i) { + si_add_header(&ctx, &ocsp_status_infos[i]); + } + apr_brigade_puts(ctx.bb, NULL, NULL, "</tr>\n</thead><tbody>"); + } + else { + ctx.prefix = "ManagedStapling"; + } + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "iterating JSON ocsp stapling status"); + md_json_itera(add_ocsp_row, &ctx, jstatus, MD_KEY_OCSPS, NULL); + if (HTML_STATUS(&ctx)) { + apr_brigade_puts(ctx.bb, NULL, NULL, "</td></tr>\n</tbody>\n</table>\n"); + } + } + + ap_pass_brigade(r->output_filters, ctx.bb); + apr_brigade_cleanup(ctx.bb); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "server-status for ocsp stapling, end"); + + return OK; +} + +/**************************************************************************************************/ +/* Status handlers */ + +int md_status_handler(request_rec *r) +{ + const md_srv_conf_t *sc; + const md_mod_conf_t *mc; + apr_array_header_t *mds; + md_json_t *jstatus; + apr_bucket_brigade *bb; + const md_t *md; + const char *name; + + if (strcmp(r->handler, "md-status")) { + return DECLINED; + } + + sc = ap_get_module_config(r->server->module_config, &md_module); + if (!sc) return DECLINED; + mc = sc->mc; + if (!mc) return DECLINED; + + if (r->method_number != M_GET) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "md-status supports only GET"); + return HTTP_NOT_IMPLEMENTED; + } + + jstatus = NULL; + md = NULL; + if (r->path_info && r->path_info[0] == '/' && r->path_info[1] != '\0') { + name = strrchr(r->path_info, '/') + 1; + md = md_get_by_name(mc->mds, name); + if (!md) md = md_get_by_domain(mc->mds, name); + } + + if (md) { + md_status_get_md_json(&jstatus, md, mc->reg, mc->ocsp, r->pool); + } + else { + mds = apr_array_copy(r->pool, mc->mds); + qsort(mds->elts, (size_t)mds->nelts, sizeof(md_t *), md_name_cmp); + md_status_get_json(&jstatus, mds, mc->reg, mc->ocsp, r->pool); + } + + if (jstatus) { + apr_table_set(r->headers_out, "Content-Type", "application/json"); + bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + md_json_writeb(jstatus, MD_JSON_FMT_INDENT, bb); + ap_pass_brigade(r->output_filters, bb); + apr_brigade_cleanup(bb); + + return DONE; + } + return DECLINED; +} + diff --git a/modules/md/mod_md_status.h b/modules/md/mod_md_status.h new file mode 100644 index 0000000..f347826 --- /dev/null +++ b/modules/md/mod_md_status.h @@ -0,0 +1,27 @@ +/* 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_status_h +#define mod_md_md_status_h + +int md_http_cert_status(request_rec *r); + +int md_domains_status_hook(request_rec *r, int flags); +int md_ocsp_status_hook(request_rec *r, int flags); + +int md_status_handler(request_rec *r); + +#endif /* mod_md_md_status_h */ |