From 0c2ae2a804dae76a991be48a6ea493e16432d809 Mon Sep 17 00:00:00 2001 From: Stefan Eissing <icing@apache.org> Date: Wed, 13 Sep 2017 14:16:49 +0000 Subject: [PATCH] On the trunk: mod_md: v0.9.5: - New directive (srly: what do you expect at this point?) "MDMustStaple on|off" to control if new certificates are requested with the OCSP Must Staple extension. - Known limitation: when the server is configured to ditch and restart child processes, for example after a certain number of connections/requests, the mod_md watchdog instance might migrate to a new child process. Since not all its state is persisted, some messsages might appear a second time in the logs. - Adding checks when 'MDRequireHttps' is used. It is considered an error when 'MDPortMap 443:-' is used - which negates that a https: port exists. Also, a warning is logged if no VirtualHost can be found for a Managed Domain that has port 443 (or the mapped one) in its address list. - New directive 'MDRequireHttps' for redirecting http: traffic to a Managed Domain, permanently or temporarily. - Fix for using a fallback certificate on initial signup of a Managed Domain. Requires also a changed mod_ssl patch (v5) to take effect. - compatibility with libressl git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1808241 13f79535-47bb-0310-9956-ffa450edef68 --- CHANGES | 18 ++ modules/md/md.h | 13 + modules/md/md_acme_authz.c | 5 +- modules/md/md_core.c | 29 +- modules/md/md_crypt.c | 121 +++++--- modules/md/md_crypt.h | 2 +- modules/md/md_reg.c | 24 ++ modules/md/md_reg.h | 25 +- modules/md/md_store_fs.c | 54 ---- modules/md/md_util.c | 107 ++++--- modules/md/md_util.h | 3 +- modules/md/md_version.h | 4 +- modules/md/mod_md.c | 563 +++++++++++++++++++++++++------------ modules/md/mod_md_config.c | 68 +++++ modules/md/mod_md_config.h | 3 + 15 files changed, 713 insertions(+), 326 deletions(-) diff --git a/CHANGES b/CHANGES index 495453acf9..bc3c4fe019 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,24 @@ -*- coding: utf-8 -*- Changes with Apache 2.5.0 + *) mod_md: v0.9.5: + - New directive (srly: what do you expect at this point?) "MDMustStaple on|off" to control if + new certificates are requested with the OCSP Must Staple extension. + - Known limitation: when the server is configured to ditch and restart child processes, for example + after a certain number of connections/requests, the mod_md watchdog instance might migrate + to a new child process. Since not all its state is persisted, some messsages might appear a + second time in the logs. + - Adding checks when 'MDRequireHttps' is used. It is considered an error when 'MDPortMap 443:-' + is used - which negates that a https: port exists. Also, a warning is logged if no + VirtualHost can be found for a Managed Domain that has port 443 (or the mapped one) in + its address list. + - New directive 'MDRequireHttps' for redirecting http: traffic to a Managed Domain, permanently + or temporarily. + - Fix for using a fallback certificate on initial signup of a Managed Domain. Requires also + a changed mod_ssl patch (v5) to take effect. + - compatibility with libressl + [Stefan Eissing] + *) htdigest: prevent a buffer overflow when a string exceeds the allowed max length in a password file. [Luca Toscano, Hanno Böck <hanno hboeck de>] diff --git a/modules/md/md.h b/modules/md/md.h index 174363a41f..fc192df977 100644 --- a/modules/md/md.h +++ b/modules/md/md.h @@ -41,6 +41,13 @@ typedef enum { MD_S_MISSING, /* MD is missing config information, cannot proceed */ } md_state_t; +typedef enum { + MD_REQUIRE_UNSET = -1, + MD_REQUIRE_OFF, + MD_REQUIRE_TEMPORARY, + MD_REQUIRE_PERMANENT, +} md_require_t; + typedef enum { MD_SV_TEXT, MD_SV_JSON, @@ -74,6 +81,8 @@ struct md_t { 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 */ @@ -119,16 +128,20 @@ struct md_t { #define MD_KEY_KEY "key" #define MD_KEY_KEYAUTHZ "keyAuthorization" #define MD_KEY_LOCATION "location" +#define MD_KEY_MUST_STAPLE "must-staple" #define MD_KEY_NAME "name" +#define MD_KEY_PERMANENT "permanent" #define MD_KEY_PKEY "privkey" #define MD_KEY_PROTO "proto" #define MD_KEY_REGISTRATION "registration" #define MD_KEY_RENEW "renew" #define MD_KEY_RENEW_WINDOW "renew-window" +#define MD_KEY_REQUIRE_HTTPS "require-https" #define MD_KEY_RESOURCE "resource" #define MD_KEY_STATE "state" #define MD_KEY_STATUS "status" #define MD_KEY_STORE "store" +#define MD_KEY_TEMPORARY "temporary" #define MD_KEY_TOKEN "token" #define MD_KEY_TRANSITIVE "transitive" #define MD_KEY_TYPE "type" diff --git a/modules/md/md_acme_authz.c b/modules/md/md_acme_authz.c index 8fbaa28b59..2a854f95b3 100644 --- a/modules/md/md_acme_authz.c +++ b/modules/md/md_acme_authz.c @@ -356,6 +356,7 @@ static apr_status_t cha_tls_sni_01_setup(md_acme_authz_cha_t *cha, md_acme_authz const char *cha_dns; apr_status_t rv; int notify_server; + apr_array_header_t *domains; if ( APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, ¬ify_server)) || APR_SUCCESS != (rv = setup_cha_dns(&cha_dns, cha, p))) { @@ -374,7 +375,9 @@ static apr_status_t cha_tls_sni_01_setup(md_acme_authz_cha_t *cha, md_acme_authz } /* setup a certificate containing the challenge dns */ - rv = md_cert_self_sign(&cha_cert, authz->domain, cha_dns, cha_key, + domains = apr_array_make(p, 5, sizeof(const char*)); + APR_ARRAY_PUSH(domains, const char*) = cha_dns; + rv = md_cert_self_sign(&cha_cert, authz->domain, domains, cha_key, apr_time_from_sec(7 * MD_SECS_PER_DAY), p); if (APR_SUCCESS != rv) { diff --git a/modules/md/md_core.c b/modules/md/md_core.c index a864633265..c079000752 100644 --- a/modules/md/md_core.c +++ b/modules/md/md_core.c @@ -85,6 +85,8 @@ md_t *md_create_empty(apr_pool_t *p) md->domains = apr_array_make(p, 5, sizeof(const char *)); md->contacts = apr_array_make(p, 5, sizeof(const char *)); md->drive_mode = MD_DRIVE_DEFAULT; + md->require_https = MD_REQUIRE_UNSET; + md->must_staple = -1; md->transitive = -1; md->defn_name = "unknown"; md->defn_line_number = 0; @@ -256,6 +258,8 @@ md_t *md_clone(apr_pool_t *p, const md_t *src) if (md) { md->state = src->state; md->name = apr_pstrdup(p, src->name); + md->require_https = src->require_https; + md->must_staple = src->must_staple; md->drive_mode = src->drive_mode; md->domains = md_array_str_compact(p, src->domains, 0); md->pkey_spec = src->pkey_spec; @@ -283,6 +287,8 @@ md_t *md_merge(apr_pool_t *p, const md_t *add, const md_t *base) 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; @@ -344,6 +350,17 @@ md_json_t *md_to_json(const md_t *md, apr_pool_t *p) na = md_array_str_compact(p, md->ca_challenges, 0); md_json_setsa(na, json, MD_KEY_CA, MD_KEY_CHALLENGES, NULL); } + switch (md->require_https) { + case MD_REQUIRE_TEMPORARY: + md_json_sets(MD_KEY_TEMPORARY, json, MD_KEY_REQUIRE_HTTPS, NULL); + break; + case MD_REQUIRE_PERMANENT: + md_json_sets(MD_KEY_PERMANENT, json, MD_KEY_REQUIRE_HTTPS, NULL); + break; + default: + break; + } + md_json_setb(md->must_staple > 0, json, MD_KEY_MUST_STAPLE, NULL); return json; } return NULL; @@ -380,7 +397,7 @@ md_t *md_from_json(md_json_t *json, apr_pool_t *p) 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) { - const char *s = md_json_gets(json, MD_KEY_RENEW_WINDOW, NULL); + s = md_json_gets(json, MD_KEY_RENEW_WINDOW, NULL); if (s && strchr(s, '%')) { int percent = atoi(s); if (0 < percent && percent < 100) { @@ -393,6 +410,16 @@ md_t *md_from_json(md_json_t *json, apr_pool_t *p) md->ca_challenges = apr_array_make(p, 5, sizeof(const char*)); md_json_dupsa(md->ca_challenges, p, json, MD_KEY_CA, MD_KEY_CHALLENGES, NULL); } + md->require_https = MD_REQUIRE_OFF; + s = md_json_gets(json, MD_KEY_REQUIRE_HTTPS, NULL); + if (s && !strcmp(MD_KEY_TEMPORARY, s)) { + md->require_https = MD_REQUIRE_TEMPORARY; + } + else if (s && !strcmp(MD_KEY_PERMANENT, s)) { + md->require_https = MD_REQUIRE_PERMANENT; + } + md->must_staple = (int)md_json_getb(json, MD_KEY_MUST_STAPLE, NULL); + return md; } return NULL; diff --git a/modules/md/md_crypt.c b/modules/md/md_crypt.c index 7b27075a8f..8084c53e6b 100644 --- a/modules/md/md_crypt.c +++ b/modules/md/md_crypt.c @@ -178,6 +178,66 @@ static int pem_passwd(char *buf, int size, int rwflag, void *baton) return ctx->pass_len; } +/**************************************************************************************************/ +/* date time things */ + +/* Get the apr time (micro seconds, since 1970) from an ASN1 time, as stored in X509 + * certificates. OpenSSL now has a utility function, but other *SSL derivatives have + * not caughts up yet or chose to ignore. An alternative is implemented, we prefer + * however the *SSL to maintain such things. + */ +static apr_time_t md_asn1_time_get(const ASN1_TIME* time) +{ +#ifdef LIBRESSL_VERSION_NUMBER + /* courtesy: https://stackoverflow.com/questions/10975542/asn1-time-to-time-t-conversion#11263731 + * all bugs are mine */ + apr_time_exp_t t; + apr_time_t ts; + const char* str = (const char*) time->data; + apr_size_t i = 0; + + memset(&t, 0, sizeof(t)); + + if (time->type == V_ASN1_UTCTIME) {/* two digit year */ + t.tm_year = (str[i++] - '0') * 10; + t.tm_year += (str[i++] - '0'); + if (t.tm_year < 70) + t.tm_year += 100; + } + else if (time->type == V_ASN1_GENERALIZEDTIME) {/* four digit year */ + t.tm_year = (str[i++] - '0') * 1000; + t.tm_year+= (str[i++] - '0') * 100; + t.tm_year+= (str[i++] - '0') * 10; + t.tm_year+= (str[i++] - '0'); + t.tm_year -= 1900; + } + t.tm_mon = (str[i++] - '0') * 10; + t.tm_mon += (str[i++] - '0') - 1; /* -1 since January is 0 not 1. */ + t.tm_mday = (str[i++] - '0') * 10; + t.tm_mday+= (str[i++] - '0'); + t.tm_hour = (str[i++] - '0') * 10; + t.tm_hour+= (str[i++] - '0'); + t.tm_min = (str[i++] - '0') * 10; + t.tm_min += (str[i++] - '0'); + t.tm_sec = (str[i++] - '0') * 10; + t.tm_sec += (str[i++] - '0'); + + if (APR_SUCCESS == apr_time_exp_gmt_get(&ts, &t)) { + return ts; + } + return 0; +#else + int secs, days; + apr_time_t ts = apr_time_now(); + + if (ASN1_TIME_diff(&days, &secs, NULL, time)) { + ts += apr_time_from_sec((days * MD_SECS_PER_DAY) + secs); + } + return ts; +#endif +} + + /**************************************************************************************************/ /* private keys */ @@ -409,7 +469,7 @@ apr_status_t md_pkey_gen(md_pkey_t **ppkey, apr_pool_t *p, md_pkey_spec_t *spec) } } -#if OPENSSL_VERSION_NUMBER < 0x10100000L +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) #ifndef NID_tlsfeature #define NID_tlsfeature 1020 @@ -658,26 +718,12 @@ int md_cert_has_expired(const md_cert_t *cert) apr_time_t md_cert_get_not_after(md_cert_t *cert) { - int secs, days; - apr_time_t time = apr_time_now(); - ASN1_TIME *not_after = X509_get_notAfter(cert->x509); - - if (ASN1_TIME_diff(&days, &secs, NULL, not_after)) { - time += apr_time_from_sec((days * MD_SECS_PER_DAY) + secs); - } - return time; + return md_asn1_time_get(X509_get_notAfter(cert->x509)); } apr_time_t md_cert_get_not_before(md_cert_t *cert) { - int secs, days; - apr_time_t time = apr_time_now(); - ASN1_TIME *not_after = X509_get_notBefore(cert->x509); - - if (ASN1_TIME_diff(&days, &secs, NULL, not_after)) { - time += apr_time_from_sec((days * MD_SECS_PER_DAY) + secs); - } - return time; + return md_asn1_time_get(X509_get_notBefore(cert->x509)); } int md_cert_covers_domain(md_cert_t *cert, const char *domain_name) @@ -991,11 +1037,6 @@ apr_status_t md_chain_fsave(apr_array_header_t *certs, apr_pool_t *p, /**************************************************************************************************/ /* certificate signing requests */ -static const char *alt_name(const char *domain, apr_pool_t *p) -{ - return apr_psprintf(p, "DNS:%s", domain); -} - static const char *alt_names(apr_array_header_t *domains, apr_pool_t *p) { const char *alts = "", *sep = "", *domain; @@ -1051,9 +1092,19 @@ static apr_status_t add_must_staple(STACK_OF(X509_EXTENSION) *exts, const md_t * { if (md->must_staple) { - X509_EXTENSION *x = X509V3_EXT_conf_nid(NULL, NULL, - NID_tlsfeature, (char*)"DER:30:03:02:01:05"); + X509_EXTENSION *x; + int nid; + + nid = OBJ_create("1.3.6.1.5.5.7.1.24", "OCSPReq", "OCSP Request"); + if (NID_undef == nid) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "%s: unable to get NID for must-staple", md->name); + return APR_EGENERAL; + } + 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 get x509 extension for must-staple", md->name); return APR_EGENERAL; } sk_X509_EXTENSION_push(exts, x); @@ -1140,7 +1191,7 @@ out: } apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, - const char *domain, md_pkey_t *pkey, + apr_array_header_t *domains, md_pkey_t *pkey, apr_interval_time_t valid_for, apr_pool_t *p) { X509 *x; @@ -1152,45 +1203,45 @@ apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, ASN1_INTEGER *asn1_rnd = NULL; unsigned char rnd[20]; - assert(domain); + assert(domains); if (NULL == (x = X509_new()) || NULL == (n = X509_NAME_new())) { rv = APR_ENOMEM; - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: openssl alloc X509 things", domain); + 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", domain); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: setup random serial", 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", domain); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: set serial number", cn); rv = APR_EGENERAL; goto out; } /* set common name and issue */ if (!X509_NAME_add_entry_by_txt(n, "CN", MBSTRING_ASC, (const unsigned char*)cn, -1, -1, 0) || !X509_set_subject_name(x, n) || !X509_set_issuer_name(x, n)) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: name add entry", domain); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: name add entry", cn); rv = APR_EGENERAL; goto out; } /* cert are uncontrained (but not very trustworthy) */ if (APR_SUCCESS != (rv = add_ext(x, NID_basic_constraints, "CA:TRUE, pathlen:0", p))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set basic constraints ext", domain); + 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_name(domain, p), p))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set alt_name ext", domain); + 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", domain); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set pkey in x509", cn); rv = APR_EGENERAL; goto out; } @@ -1204,7 +1255,7 @@ apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, /* sign with same key */ if (!X509_sign(x, pkey->pkey, EVP_sha256())) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign x509", domain); + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign x509", cn); rv = APR_EGENERAL; goto out; } diff --git a/modules/md/md_crypt.h b/modules/md/md_crypt.h index 401e6403a3..fc7c2d1dd3 100644 --- a/modules/md/md_crypt.h +++ b/modules/md/md_crypt.h @@ -127,7 +127,7 @@ apr_status_t md_cert_req_create(const char **pcsr_der_64, const struct md_t *md, md_pkey_t *pkey, apr_pool_t *p); apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, - const char *domain, md_pkey_t *pkey, + struct apr_array_header_t *domains, md_pkey_t *pkey, apr_interval_time_t valid_for, apr_pool_t *p); #endif /* md_crypt_h */ diff --git a/modules/md/md_reg.c b/modules/md/md_reg.c index 4d1e217176..7d45f1ef18 100644 --- a/modules/md/md_reg.c +++ b/modules/md/md_reg.c @@ -506,6 +506,18 @@ static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, v nmd->pkey_spec = apr_pmemdup(p, updates->pkey_spec, sizeof(md_pkey_spec_t)); } } + if (MD_UPD_REQUIRE_HTTPS & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update require-https: %s", name); + nmd->require_https = updates->require_https; + } + if (MD_UPD_TRANSITIVE & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update transitive: %s", name); + nmd->transitive = updates->transitive; + } + if (MD_UPD_MUST_STAPLE & fields) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update must-staple: %s", name); + nmd->must_staple = updates->must_staple; + } if (fields && APR_SUCCESS == (rv = md_save(reg->store, p, MD_SG_DOMAINS, nmd, 0))) { rv = state_init(reg, ptemp, nmd, 0); @@ -743,6 +755,10 @@ apr_status_t md_reg_sync(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, 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; @@ -780,6 +796,14 @@ apr_status_t md_reg_sync(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, 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); diff --git a/modules/md/md_reg.h b/modules/md/md_reg.h index 9d5284c435..5007c6d888 100644 --- a/modules/md/md_reg.h +++ b/modules/md/md_reg.h @@ -82,18 +82,21 @@ 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_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_ALL 0x7FFFFFFF +#define MD_UPD_PKEY_SPEC 0x0400 +#define MD_UPD_REQUIRE_HTTPS 0x0800 +#define MD_UPD_TRANSITIVE 0x1000 +#define MD_UPD_MUST_STAPLE 0x2000 +#define MD_UPD_ALL 0x7FFFFFFF /** * Update the given fields for the managed domain. Take the new diff --git a/modules/md/md_store_fs.c b/modules/md/md_store_fs.c index 0ba017c256..73ed62e849 100644 --- a/modules/md/md_store_fs.c +++ b/modules/md/md_store_fs.c @@ -261,57 +261,6 @@ read: return rv; } -static apr_status_t setup_fallback_cert(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) -{ - md_store_fs_t *s_fs = baton; - md_pkey_t *fallback_key; - md_cert_t *fallback_cert; - md_pkey_spec_t spec; - apr_status_t rv; - - if (APR_SUCCESS == (rv = fs_load(&s_fs->s, MD_SG_NONE, NULL, MD_FN_FALLBACK_PKEY, - MD_SV_PKEY, (void**)&fallback_key, ptemp)) - && APR_SUCCESS == (rv = fs_load(&s_fs->s, MD_SG_NONE, NULL, MD_FN_FALLBACK_CERT, - MD_SV_CERT, (void**)&fallback_cert, ptemp))) { - apr_time_t not_after = md_cert_get_not_after(fallback_cert); - if (not_after > apr_time_now() + apr_time_from_sec(7 * MD_SECS_PER_DAY)) { - /* at least a week more valid, expect drive and restart way before that */ - return APR_SUCCESS; - } - } - - spec.type = MD_PKEY_TYPE_RSA; - spec.params.rsa.bits = MD_PKEY_RSA_BITS_DEF; - - if (APR_SUCCESS != (rv = md_pkey_gen(&fallback_key, ptemp, &spec))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "create fallback key"); - return rv; - } - - if (APR_SUCCESS != (rv = md_store_save(&s_fs->s, ptemp, MD_SG_NONE, NULL, - MD_FN_FALLBACK_PKEY, MD_SV_PKEY, - (void*)fallback_key, 0))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "save fallback key"); - return rv; - } - - if (APR_SUCCESS != (rv = md_cert_self_sign(&fallback_cert, "Apache Managed Domain Fallback", - "temporary.invalid.certificate", fallback_key, - apr_time_from_sec(14 * MD_SECS_PER_DAY), ptemp))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "create fallback certificate"); - return rv; - } - - if (APR_SUCCESS != (rv = md_store_save(&s_fs->s, ptemp, MD_SG_NONE, NULL, - MD_FN_FALLBACK_CERT, MD_SV_CERT, - (void*)fallback_cert, 0))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "save fallback certificate"); - return rv; - } - - return rv; -} - apr_status_t md_store_fs_init(md_store_t **pstore, apr_pool_t *p, const char *path) { md_store_fs_t *s_fs; @@ -356,9 +305,6 @@ apr_status_t md_store_fs_init(md_store_t **pstore, apr_pool_t *p, const char *pa } } rv = md_util_pool_vdo(setup_store_file, s_fs, p, NULL); - if (APR_SUCCESS == rv) { - rv = md_util_pool_vdo(setup_fallback_cert, s_fs, p, NULL); - } if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "init fs store at %s", path); diff --git a/modules/md/md_util.c b/modules/md/md_util.c index 756aaef382..875cef61fc 100644 --- a/modules/md/md_util.c +++ b/modules/md/md_util.c @@ -647,52 +647,54 @@ const char *md_util_schemify(apr_pool_t *p, const char *s, const char *def_schem return apr_psprintf(p, "%s:%s", def_scheme, s); } -apr_status_t md_util_abs_uri_check(apr_pool_t *p, const char *uri, const char **perr) +static apr_status_t uri_check(apr_uri_t *uri_parsed, apr_pool_t *p, + const char *uri, const char **perr) { const char *s, *err = NULL; - apr_uri_t uri_parsed; apr_status_t rv; - if (APR_SUCCESS != (rv = apr_uri_parse(p, uri, &uri_parsed))) { + if (APR_SUCCESS != (rv = apr_uri_parse(p, uri, uri_parsed))) { err = "not an uri"; } - else if (!uri_parsed.scheme) { - err = "missing uri scheme"; - } - else if (strlen(uri_parsed.scheme) + 1 >= strlen(uri)) { - err = "missing uri identifier"; - } - else if (strchr(uri, ' ') || strchr(uri, '\t') ) { - err = "whitespace in uri"; - } - else if (!strncmp("http", uri_parsed.scheme, 4)) { - if (!uri_parsed.hostname) { - err = "missing hostname"; + else if (uri_parsed->scheme) { + if (strlen(uri_parsed->scheme) + 1 >= strlen(uri)) { + err = "missing uri identifier"; } - else if (!md_util_is_dns_name(p, uri_parsed.hostname, 0)) { - err = "invalid hostname"; + else if (!strncmp("http", uri_parsed->scheme, 4)) { + if (!uri_parsed->hostname) { + err = "missing hostname"; + } + else if (!md_util_is_dns_name(p, uri_parsed->hostname, 0)) { + err = "invalid hostname"; + } + if (uri_parsed->port_str + && (!apr_isdigit(uri_parsed->port_str[0]) + || uri_parsed->port == 0 + || uri_parsed->port > 65353)) { + err = "invalid port"; + } } - if (uri_parsed.port_str && (uri_parsed.port == 0 || uri_parsed.port > 65353)) { - err = "invalid port"; + else if (!strcmp("mailto", uri_parsed->scheme)) { + s = strchr(uri, '@'); + if (!s) { + err = "missing @"; + } + else if (strchr(s+1, '@')) { + err = "duplicate @"; + } + else if (s == uri + strlen(uri_parsed->scheme) + 1) { + err = "missing local part"; + } + else if (s == (uri + strlen(uri)-1)) { + err = "missing hostname"; + } + else if (strstr(uri, "..")) { + err = "double period"; + } } } - else if (!strcmp("mailto", uri_parsed.scheme)) { - s = strchr(uri, '@'); - if (!s) { - err = "missing @"; - } - else if (strchr(s+1, '@')) { - err = "duplicate @"; - } - else if (s == uri + strlen(uri_parsed.scheme) + 1) { - err = "missing local part"; - } - else if (s == (uri + strlen(uri)-1)) { - err = "missing hostname"; - } - else if (strstr(uri, "..")) { - err = "double period"; - } + if (strchr(uri, ' ') || strchr(uri, '\t') ) { + err = "whitespace in uri"; } if (err) { @@ -702,6 +704,39 @@ apr_status_t md_util_abs_uri_check(apr_pool_t *p, const char *uri, const char ** return rv; } +apr_status_t md_util_abs_uri_check(apr_pool_t *p, const char *uri, const char **perr) +{ + apr_uri_t uri_parsed; + apr_status_t rv; + + if (APR_SUCCESS == (rv = uri_check(&uri_parsed, p, uri, perr))) { + if (!uri_parsed.scheme) { + *perr = "missing uri scheme"; + return APR_EINVAL; + } + } + return rv; +} + +apr_status_t md_util_abs_http_uri_check(apr_pool_t *p, const char *uri, const char **perr) +{ + apr_uri_t uri_parsed; + apr_status_t rv; + + if (APR_SUCCESS == (rv = uri_check(&uri_parsed, p, uri, perr))) { + if (!uri_parsed.scheme) { + *perr = "missing uri scheme"; + return APR_EINVAL; + } + if (apr_strnatcasecmp("http", uri_parsed.scheme) + && apr_strnatcasecmp("https", uri_parsed.scheme)) { + *perr = "uri scheme must be http or https"; + return APR_EINVAL; + } + } + return rv; +} + /* retry login ************************************************************************************/ apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs, diff --git a/modules/md/md_util.h b/modules/md/md_util.h index dfdc8d89e7..1f384b5a71 100644 --- a/modules/md/md_util.h +++ b/modules/md/md_util.h @@ -117,7 +117,8 @@ apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, const char *md_util_schemify(apr_pool_t *p, const char *s, const char *def_scheme); apr_status_t md_util_abs_uri_check(apr_pool_t *p, const char *s, const char **perr); - +apr_status_t md_util_abs_http_uri_check(apr_pool_t *p, const char *uri, const char **perr); + const char *md_link_find_relation(const struct apr_table_t *headers, apr_pool_t *pool, const char *relation); diff --git a/modules/md/md_version.h b/modules/md/md_version.h index 43576676d7..f345721813 100644 --- a/modules/md/md_version.h +++ b/modules/md/md_version.h @@ -26,7 +26,7 @@ * @macro * Version number of the md module as c string */ -#define MOD_MD_VERSION "0.9.2-git" +#define MOD_MD_VERSION "0.9.5" /** * @macro @@ -34,7 +34,7 @@ * 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 0x000902 +#define MOD_MD_VERSION_NUM 0x000905 #define MD_EXPERIMENTAL 0 #define MD_ACME_DEF_URL "https://acme-v01.api.letsencrypt.org/directory" diff --git a/modules/md/mod_md.c b/modules/md/mod_md.c index 74252881db..787d82e9d4 100644 --- a/modules/md/mod_md.c +++ b/modules/md/mod_md.c @@ -14,11 +14,13 @@ */ #include <assert.h> +#include <apr_optional.h> #include <apr_strings.h> #include <ap_release.h> #include <mpm_common.h> #include <httpd.h> +#include <http_core.h> #include <http_protocol.h> #include <http_request.h> #include <http_log.h> @@ -41,6 +43,7 @@ #include "mod_md.h" #include "mod_md_config.h" #include "mod_md_os.h" +#include "mod_ssl.h" #include "mod_watchdog.h" static void md_hooks(apr_pool_t *pool); @@ -92,6 +95,12 @@ static void md_merge_srv(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p) md->pkey_spec = md->sc->pkey_spec; } + if (md->require_https < 0) { + md->require_https = md_config_geti(md->sc, MD_CONFIG_REQUIRE_HTTPS); + } + if (md->must_staple < 0) { + md->must_staple = md_config_geti(md->sc, MD_CONFIG_MUST_STAPLE); + } } static apr_status_t check_coverage(md_t *md, const char *domain, server_rec *s, apr_pool_t *p) @@ -113,28 +122,61 @@ static apr_status_t check_coverage(md_t *md, const char *domain, server_rec *s, } } -static apr_status_t apply_to_servers(md_t *md, server_rec *base_server, +static apr_status_t md_covers_server(md_t *md, server_rec *s, apr_pool_t *p) +{ + apr_status_t rv; + const char *name; + int i; + + if (APR_SUCCESS == (rv = check_coverage(md, s->server_hostname, s, p)) && s->names) { + for (i = 0; i < s->names->nelts; ++i) { + name = APR_ARRAY_IDX(s->names, i, const char*); + if (APR_SUCCESS != (rv = check_coverage(md, name, s, p))) { + break; + } + } + } + return rv; +} + +static int matches_port_somewhere(server_rec *s, int port) +{ + server_addr_rec *sa; + + for (sa = s->addrs; sa; sa = sa->next) { + if (sa->host_port == port) { + /* host_addr might be general (0.0.0.0) or specific, we count this as match */ + return 1; + } + if (sa->host_port == 0) { + /* wildcard port, answers to all ports. Rare, but may work. */ + return 1; + } + } + return 0; +} + +static apr_status_t assign_to_servers(md_t *md, server_rec *base_server, apr_pool_t *p, apr_pool_t *ptemp) { - server_rec *s; + server_rec *s, *s_https; request_rec r; md_srv_conf_t *sc; md_mod_conf_t *mc; - apr_status_t rv = APR_SUCCESS, rv2; - int i, j; - const char *domain, *name; + apr_status_t rv = APR_SUCCESS; + int i; + const char *domain; + apr_array_header_t *servers; sc = md_config_get(base_server); mc = sc->mc; - - /* Find the (at most one) managed domain for each vhost/base server and - * remember it at our config for it. - * The config is not accepted, if a vhost matches 2 or more managed domains. + + /* 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)); - sc = NULL; + servers = apr_array_make(ptemp, 5, sizeof(server_rec*)); - /* This MD may apply to 0, 1 or more sever_recs */ for (s = base_server; s; s = s->next) { r.server = s; @@ -142,8 +184,7 @@ static apr_status_t apply_to_servers(md_t *md, server_rec *base_server, domain = APR_ARRAY_IDX(md->domains, i, const char*); if (ap_matches_request_vhost(&r, domain, s->port)) { - /* Create a unique md_srv_conf_t record for this server. - * We keep local information here. */ + /* 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) @@ -158,54 +199,91 @@ static apr_status_t apply_to_servers(md_t *md, server_rec *base_server, 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); - rv = APR_EINVAL; - goto next_server; + return APR_EINVAL; + } + + /* If server has name or an alias not covered, + * a generated certificate will not match. + */ + if (APR_SUCCESS != (rv = md_covers_server(md, s, ptemp))) { + return rv; } + + sc->assigned = md; + APR_ARRAY_PUSH(servers, server_rec*) = s; ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10043) "Managed Domain %s applies to vhost %s:%d", md->name, s->server_hostname, s->port); - /* If there is a non-default ServerAdmin defined for this vhost, take - * that one as contact info */ + 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)) { - apr_array_clear(md->contacts); - APR_ARRAY_PUSH(md->contacts, const char *) = - md_util_schemify(p, s->server_admin, "mailto"); - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10044) - "Managed Domain %s assigned server admin %s", md->name, - s->server_admin); + uri = md_util_schemify(p, s->server_admin, "mailto"); + if (md_array_str_index(md->contacts, uri, 0, 0) < 0) { + APR_ARRAY_PUSH(md->contacts, const char *) = uri; + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10044) + "%s: added contact %s", md->name, uri); + } + } + } + + if (md->require_https > MD_REQUIRE_OFF) { + /* We require https for this MD, but do we have port 443 (or a mapped one) + * available? */ + if (mc->local_443 <= 0) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO() + "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; + } - /* remember */ - sc->assigned = md; - /* This server matches a managed domain. If it contains names or - * alias that are not in this md, a generated certificate will not match. */ - if (APR_SUCCESS == (rv2 = check_coverage(md, s->server_hostname, s, p)) - && s->names) { - for (j = 0; j < s->names->nelts; ++j) { - name = APR_ARRAY_IDX(s->names, j, const char*); - if (APR_SUCCESS != (rv2 = check_coverage(md, name, s, p))) { - break; - } + /* Ok, we know which local port represents 443, do we have a server_rec + * for MD that has addresses with port 443? */ + s_https = NULL; + for (i = 0; i < servers->nelts; ++i) { + s = APR_ARRAY_IDX(servers, i, server_rec*); + if (matches_port_somewhere(s, mc->local_443)) { + s_https = s; + break; } } - if (APR_SUCCESS != rv2) { - rv = rv2; + 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() + "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); } - goto next_server; } } - next_server: - continue; - } - - if (sc == NULL && 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; + } return rv; } @@ -267,9 +345,9 @@ static apr_status_t md_calc_md_list(apr_pool_t *p, apr_pool_t *plog, } } - /* Apply to the vhost(s) that this MD matches - if any. Perform some + /* Assign MD to the server_rec configs that it matches. Perform some * last finishing touches on the MD. */ - if (APR_SUCCESS != (rv = apply_to_servers(md, base_server, p, ptemp))) { + if (APR_SUCCESS != (rv = assign_to_servers(md, base_server, p, ptemp))) { return rv; } @@ -432,6 +510,16 @@ static void init_setups(apr_pool_t *p, server_rec *base_server) apr_pool_cleanup_register(p, NULL, cleanup_setups, apr_pool_cleanup_null); } +/**************************************************************************************************/ +/* mod_ssl interface */ + +static APR_OPTIONAL_FN_TYPE(ssl_is_https) *opt_ssl_is_https; + +static void init_ssl(void) +{ + opt_ssl_is_https = APR_RETRIEVE_OPTIONAL_FN(ssl_is_https); +} + /**************************************************************************************************/ /* watchdog based impl. */ @@ -441,73 +529,131 @@ 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; ap_watchdog_t *watchdog; - int all_valid; - apr_time_t valid_not_before; - int error_count; - int processed_count; - - int error_runs; + apr_time_t next_change; - apr_time_t next_valid; - apr_array_header_t *mds; + apr_array_header_t *jobs; md_reg_t *reg; } md_watchdog; -static apr_status_t drive_md(md_watchdog *wd, md_t *md, apr_pool_t *ptemp) +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 check_job(md_watchdog *wd, md_job_t *job, apr_pool_t *ptemp) { apr_status_t rv = APR_SUCCESS; - apr_time_t renew_time, now, valid_from; + apr_time_t valid_from, delay; int errored, renew; char ts[APR_RFC822_DATE_LEN]; - if (md->state == MD_S_MISSING) { - rv = APR_INCOMPLETE; + if (apr_time_now() < job->next_check) { + /* Job needs to wait */ + return APR_EAGAIN; + } + + job->next_check = 0; + 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 */ + return APR_INCOMPLETE; } - if (md->state == MD_S_COMPLETE && !md->expires) { - /* This is our indicator that we did already renewed this managed domain - * successfully and only wait on the next restart for it to activate */ - now = apr_time_now(); - ap_log_error( APLOG_MARK, APLOG_INFO, 0, wd->s, APLOGNO(10051) - "md(%s): has been renewed, should be activated in %s", - md->name, (md->valid_from <= now)? "about now" : - md_print_duration(ptemp, md->valid_from - now)); + else if (job->renewed) { + assess_renewal(wd, job, ptemp); } - else if (APR_SUCCESS == (rv = md_reg_assess(wd->reg, md, &errored, &renew, wd->p))) { + 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", md->name); + "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", md->name, md->state); + "md(%s): state=%d, driving", job->md->name, job->md->state); - rv = md_reg_stage(wd->reg, md, NULL, 0, &valid_from, ptemp); + rv = md_reg_stage(wd->reg, job->md, NULL, 0, &valid_from, ptemp); if (APR_SUCCESS == rv) { - md->state = MD_S_COMPLETE; - md->expires = 0; - md->valid_from = valid_from; - ++wd->processed_count; - if (!wd->next_valid || wd->next_valid > valid_from) { - wd->next_valid = valid_from; - } + job->renewed = 1; + job->restart_at = valid_from; + assess_renewal(wd, job, ptemp); } } else { - apr_rfc822_date(ts, md->expires); + job->next_check = job->md->expires - job->md->renew_window; + + apr_rfc822_date(ts, job->md->expires); ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10053) - "md(%s): is complete, cert expires %s", md->name, ts); - renew_time = md->expires - md->renew_window; - if (renew_time < wd->next_change) { - wd->next_change = renew_time; - } + "md(%s): is complete, cert expires %s", job->md->name, ts); } } + + if (APR_SUCCESS == rv) { + job->error_runs = 0; + } + else { + ap_log_error( APLOG_MARK, APLOG_ERR, rv, wd->s, APLOGNO(10056) + "processing %s", job->md->name); + ++job->error_runs; + /* back off duration, depending on the errors we encounter in a row */ + delay = apr_time_from_sec(5 << (job->error_runs - 1)); + if (delay > apr_time_from_sec(60*60)) { + delay = apr_time_from_sec(60*60); + } + job->next_check = apr_time_now() + delay; + ap_log_error(APLOG_MARK, APLOG_INFO, 0, wd->s, APLOGNO(10057) + "%s: encountered error for the %d. time, next run in %s", + job->md->name, job->error_runs, md_print_duration(ptemp, delay)); + } + + job->last_rv = rv; return rv; } @@ -515,97 +661,50 @@ static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp) { md_watchdog *wd = baton; apr_status_t rv = APR_SUCCESS; - md_t *md; + 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->mds->nelts); + "md watchdog start, auto drive %d mds", wd->jobs->nelts); break; case AP_WATCHDOG_STATE_RUNNING: assert(wd->reg); - wd->all_valid = 1; - wd->valid_not_before = 0; - wd->processed_count = 0; - wd->error_count = 0; wd->next_change = 0; - wd->next_valid = 0; - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10055) - "md watchdog run, auto drive %d mds", wd->mds->nelts); + "md watchdog run, auto drive %d mds", wd->jobs->nelts); - /* Check if all Managed Domains are ok or if we have to do something */ - for (i = 0; i < wd->mds->nelts; ++i) { - md = APR_ARRAY_IDX(wd->mds, i, md_t *); - - rv = drive_md(wd, md, ptemp); + /* 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 *); - if (APR_STATUS_IS_INCOMPLETE(rv)) { - /* configuration not complete, this MD cannot be driven further */ - wd->all_valid = 0; + rv = check_job(wd, job, ptemp); + + if (job->need_restart && !job->restart_processed) { + restart = 1; } - else if (APR_SUCCESS != rv) { - wd->all_valid = 0; - ++wd->error_count; - ap_log_error( APLOG_MARK, APLOG_ERR, rv, wd->s, APLOGNO(10056) - "processing %s", md->name); + if (job->next_check && job->next_check < next_run) { + next_run = job->next_check; } } - /* Determine when we want to run next */ - wd->error_runs = wd->error_count? (wd->error_runs + 1) : 0; - - if (wd->all_valid) { - ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, wd->s, "all managed domains are valid"); - } - else if (wd->error_count == 0) { - ap_log_error(APLOG_MARK, APLOG_INFO, 0, wd->s, APLOGNO() - "all managed domains driven as far as possible"); - } - now = apr_time_now(); - /* normally, we'd like to run at least twice a day */ - next_run = now + apr_time_from_sec(MD_SECS_PER_DAY / 2); - - /* Unless we know of an MD change before that */ - if (wd->next_change > 0 && wd->next_change < next_run) { - next_run = wd->next_change; - } - - /* Or have to activate a new cert even before that */ - if (wd->next_valid > now && wd->next_valid < next_run) { - next_run = wd->next_valid; - ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, wd->s, - "Delaying activation of %d Managed Domain%s by %s", - wd->processed_count, (wd->processed_count > 1)? "s have" : " has", - md_print_duration(ptemp, next_run - now)); - } - - /* Or encountered errors and like to retry even before that */ - if (wd->error_count > 0) { - apr_interval_time_t delay; - - /* back off duration, depending on the errors we encounter in a row */ - delay = apr_time_from_sec(5 << (wd->error_runs - 1)); - if (delay > apr_time_from_sec(60*60)) { - delay = apr_time_from_sec(60*60); - } - if (now + delay < next_run) { - ap_log_error(APLOG_MARK, APLOG_INFO, 0, wd->s, APLOGNO(10057) - "encountered errors for the %d. time, next try by %s", - wd->error_runs, md_print_duration(ptemp, delay)); - next_run = now + delay; - } - } - if (APLOGdebug(wd->s)) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO() "next run in %s", md_print_duration(ptemp, next_run - now)); } wd_set_interval(wd->watchdog, next_run - now, wd, run_watchdog); + + for (i = 0; i < wd->jobs->nelts; ++i) { + job = APR_ARRAY_IDX(wd->jobs, i, md_job_t *); + } break; case AP_WATCHDOG_STATE_STOPPING: @@ -614,31 +713,31 @@ static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp) break; } - if (wd->processed_count) { - now = apr_time_now(); + if (restart) { + const char *action, *names = ""; + int n; - if (wd->all_valid) { - if (wd->next_valid <= now) { - rv = md_server_graceful(ptemp, wd->s); - if (APR_ENOTIMPL == rv) { - /* self-graceful restart not supported in this setup */ - ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, wd->s, APLOGNO(10059) - "%d Managed Domain%s been setup and changes will be " - "activated on next (graceful) server restart.", - wd->processed_count, (wd->processed_count > 1)? "s have" : " has"); - } + 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; + job->restart_processed = 1; + } + } + + if (n > 0) { + 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 { - /* activation is delayed */ + action = " and server has been asked to restart now."; } - } - else { - ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, wd->s, APLOGNO(10060) - "%d Managed Domain%s been setup, while %d%s " - "still being worked on. You may activate the changes made " - "by triggering a (graceful) restart at any time.", - wd->processed_count, (wd->processed_count > 1)? "s have" : " has", - wd->error_count, (wd->error_count > 1)? " are" : " is"); + 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); } } @@ -654,6 +753,7 @@ static apr_status_t start_watchdog(apr_array_header_t *names, apr_pool_t *p, 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); @@ -681,7 +781,7 @@ static apr_status_t start_watchdog(apr_array_header_t *names, apr_pool_t *p, wd->reg = reg; wd->s = s; - wd->mds = apr_array_make(wd->p, 10, sizeof(md_t *)); + 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); @@ -692,14 +792,18 @@ static apr_status_t start_watchdog(apr_array_header_t *names, apr_pool_t *p, "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); - APR_ARRAY_PUSH(wd->mds, md_t*) = md; } } } - if (!wd->mds->nelts) { + 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"); @@ -819,6 +923,8 @@ static apr_status_t md_post_config(apr_pool_t *p, apr_pool_t *plog, } } + init_ssl(); + /* If there are MDs to drive, start a watchdog to check on them regularly */ if (drive_names->nelts > 0) { ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, APLOGNO(10074) @@ -854,6 +960,34 @@ static int md_is_managed(server_rec *s) return 0; } +static apr_status_t setup_fallback_cert(md_store_t *store, const md_t *md, apr_pool_t *p) +{ + md_pkey_t *pkey; + md_cert_t *cert; + md_pkey_spec_t spec; + apr_status_t rv; + + spec.type = MD_PKEY_TYPE_RSA; + spec.params.rsa.bits = MD_PKEY_RSA_BITS_DEF; + + if ( APR_SUCCESS == (rv = md_pkey_gen(&pkey, p, &spec)) + && APR_SUCCESS == (rv = md_store_save(store, p, MD_SG_DOMAINS, md->name, + MD_FN_FALLBACK_PKEY, 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))) { + rv = md_store_save(store, p, MD_SG_DOMAINS, md->name, + MD_FN_FALLBACK_CERT, MD_SV_CERT, (void*)cert, 0); + } + + 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) { @@ -871,24 +1005,38 @@ static apr_status_t md_get_certificate(server_rec *s, apr_pool_t *p, assert(sc->mc); assert(sc->mc->store); if (APR_SUCCESS != (rv = md_reg_init(®, p, sc->mc->store, sc->mc->proxy_url))) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO() "init registry"); return rv; } md = md_reg_get(reg, sc->assigned->name, p); if (APR_SUCCESS != (rv = md_reg_get_cred_files(reg, md, p, pkeyfile, pcertfile))) { + ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO() + "retrieving credentials for MD %s", md->name); return rv; } - if (!*pkeyfile || !*pcertfile - || APR_SUCCESS != md_util_is_file(*pkeyfile, p) - || APR_SUCCESS != md_util_is_file(*pcertfile, p)) { + 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. */ - md_store_get_fname(pkeyfile, sc->mc->store, MD_SG_NONE, NULL, MD_FN_FALLBACK_PKEY, p); - md_store_get_fname(pcertfile, sc->mc->store, MD_SG_NONE, NULL, MD_FN_FALLBACK_CERT, p); + + md_store_get_fname(pkeyfile, sc->mc->store, MD_SG_DOMAINS, + md->name, MD_FN_FALLBACK_PKEY, p); + md_store_get_fname(pcertfile, sc->mc->store, MD_SG_DOMAINS, + md->name, MD_FN_FALLBACK_CERT, p); + if (!fexists(*pkeyfile, p) || !fexists(*pcertfile, p)) { + if (APR_SUCCESS != (rv = setup_fallback_cert(sc->mc->store, md, p))) { + ap_log_error(APLOG_MARK, APLOG_TRACE1, rv, s, + "%s: setup fallback certificate", md->name); + return rv; + } + } + ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, + "%s: providing fallback certificate for server %s", + md->name, s->server_hostname); return APR_EAGAIN; } @@ -957,7 +1105,8 @@ static int md_is_challenge(conn_rec *c, const char *servername, /**************************************************************************************************/ /* ACME challenge responses */ -#define ACME_CHALLENGE_PREFIX "/.well-known/acme-challenge/" +#define WELL_KNOWN_PREFIX "/.well-known/" +#define ACME_CHALLENGE_PREFIX WELL_KNOWN_PREFIX"acme-challenge/" static int md_http_challenge_pr(request_rec *r) { @@ -965,14 +1114,13 @@ static int md_http_challenge_pr(request_rec *r) const md_srv_conf_t *sc; const char *name, *data; apr_status_t rv; - + if (!strncmp(ACME_CHALLENGE_PREFIX, r->parsed_uri.path, sizeof(ACME_CHALLENGE_PREFIX)-1)) { if (r->method_number == M_GET) { md_store_t *store; sc = ap_get_module_config(r->server->module_config, &md_module); store = sc? sc->mc->store : NULL; - name = r->parsed_uri.path + sizeof(ACME_CHALLENGE_PREFIX)-1; r->status = HTTP_NOT_FOUND; @@ -1011,6 +1159,51 @@ static int md_http_challenge_pr(request_rec *r) return DECLINED; } +/**************************************************************************************************/ +/* Require Https hook */ + +static int md_require_https_maybe(request_rec *r) +{ + const md_srv_conf_t *sc; + apr_uri_t uri; + const char *s; + int status; + + if (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 + && opt_ssl_is_https && !opt_ssl_is_https(r->connection)) { + /* Do not have https:, but require it. Redirect the request accordingly. + */ + if (r->method_number == M_GET) { + /* safe to use the old-fashioned codes */ + status = ((MD_REQUIRE_PERMANENT == sc->assigned->require_https)? + HTTP_MOVED_PERMANENTLY : HTTP_MOVED_TEMPORARILY); + } + else { + /* these should keep the method unchanged on retry */ + status = ((MD_REQUIRE_PERMANENT == sc->assigned->require_https)? + HTTP_PERMANENT_REDIRECT : HTTP_TEMPORARY_REDIRECT); + } + + s = ap_construct_url(r->pool, r->uri, r); + if (APR_SUCCESS == apr_uri_parse(r->pool, s, &uri)) { + uri.scheme = (char*)"https"; + uri.port = 443; + uri.port_str = (char*)"443"; + uri.query = r->parsed_uri.query; + uri.fragment = r->parsed_uri.fragment; + s = apr_uri_unparse(r->pool, &uri, APR_URI_UNP_OMITUSERINFO); + if (s && *s) { + apr_table_setn(r->headers_out, "Location", s); + return status; + } + } + } + } + return DECLINED; +} + /* Runs once per created child process. Perform any process * related initionalization here. */ @@ -1039,6 +1232,8 @@ static void md_hooks(apr_pool_t *pool) /* answer challenges *very* early, before any configured authentication may strike */ ap_hook_post_read_request(md_http_challenge_pr, NULL, NULL, APR_HOOK_MIDDLE); + /* redirect to https if configured */ + ap_hook_fixups(md_require_https_maybe, NULL, NULL, APR_HOOK_MIDDLE); APR_REGISTER_OPTIONAL_FN(md_is_managed); APR_REGISTER_OPTIONAL_FN(md_get_certificate); diff --git a/modules/md/mod_md_config.c b/modules/md/mod_md_config.c index e309121367..36feec620b 100644 --- a/modules/md/mod_md_config.c +++ b/modules/md/mod_md_config.c @@ -39,10 +39,12 @@ #define MD_CMD_DRIVEMODE "MDDriveMode" #define MD_CMD_MEMBER "MDMember" #define MD_CMD_MEMBERS "MDMembers" +#define MD_CMD_MUST_STAPLE "MDMustStaple" #define MD_CMD_PORTMAP "MDPortMap" #define MD_CMD_PKEYS "MDPrivateKeys" #define MD_CMD_PROXY "MDHttpProxy" #define MD_CMD_RENEWWINDOW "MDRenewWindow" +#define MD_CMD_REQUIREHTTPS "MDRequireHttps" #define MD_CMD_STOREDIR "MDStoreDir" #define DEF_VAL (-1) @@ -67,6 +69,7 @@ static md_srv_conf_t defconf = { &defmc, 1, + MD_REQUIRE_OFF, MD_DRIVE_AUTO, 0, NULL, @@ -112,6 +115,7 @@ static md_mod_conf_t *md_mod_conf_get(apr_pool_t *pool, int create) static void srv_conf_props_clear(md_srv_conf_t *sc) { sc->transitive = DEF_VAL; + sc->require_https = MD_REQUIRE_UNSET; sc->drive_mode = DEF_VAL; sc->must_staple = DEF_VAL; sc->pkey_spec = NULL; @@ -126,6 +130,7 @@ static void srv_conf_props_clear(md_srv_conf_t *sc) static void srv_conf_props_copy(md_srv_conf_t *to, const md_srv_conf_t *from) { to->transitive = from->transitive; + to->require_https = from->require_https; to->drive_mode = from->drive_mode; to->must_staple = from->must_staple; to->pkey_spec = from->pkey_spec; @@ -139,6 +144,7 @@ static void srv_conf_props_copy(md_srv_conf_t *to, const md_srv_conf_t *from) static void srv_conf_props_apply(md_t *md, const md_srv_conf_t *from, apr_pool_t *p) { + if (from->require_https != MD_REQUIRE_UNSET) md->require_https = from->require_https; if (from->transitive != DEF_VAL) md->transitive = from->transitive; if (from->drive_mode != DEF_VAL) md->drive_mode = from->drive_mode; if (from->must_staple != DEF_VAL) md->must_staple = from->must_staple; @@ -176,6 +182,7 @@ static void *md_config_merge(apr_pool_t *pool, void *basev, void *addv) nsc->name = name; nsc->transitive = (add->transitive != DEF_VAL)? add->transitive : base->transitive; + nsc->require_https = (add->require_https != MD_REQUIRE_UNSET)? add->require_https : base->require_https; nsc->drive_mode = (add->drive_mode != DEF_VAL)? add->drive_mode : base->drive_mode; nsc->must_staple = (add->must_staple != DEF_VAL)? add->must_staple : base->must_staple; nsc->pkey_spec = add->pkey_spec? add->pkey_spec : base->pkey_spec; @@ -426,6 +433,55 @@ static const char *md_config_set_drive_mode(cmd_parms *cmd, void *dc, const char return NULL; } +static const char *md_config_set_must_staple(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + + if (!inside_section(cmd, MD_CMD_MD_SECTION) + && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + + if (!apr_strnatcasecmp("off", value)) { + config->must_staple = 0; + } + else if (!apr_strnatcasecmp("on", value)) { + config->must_staple = 1; + } + else { + return apr_pstrcat(cmd->pool, "unknown '", value, + "', supported parameter values are 'on' and 'off'", NULL); + } + return NULL; +} + +static const char *md_config_set_require_https(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err; + + if (!inside_section(cmd, MD_CMD_MD_SECTION) + && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + + if (!apr_strnatcasecmp("off", value)) { + config->require_https = MD_REQUIRE_OFF; + } + else if (!apr_strnatcasecmp(MD_KEY_TEMPORARY, value)) { + config->require_https = MD_REQUIRE_TEMPORARY; + } + else if (!apr_strnatcasecmp(MD_KEY_PERMANENT, value)) { + config->require_https = MD_REQUIRE_PERMANENT; + } + else { + return apr_pstrcat(cmd->pool, "unknown '", value, + "', supported parameter values are 'temporary' and 'permanent'", NULL); + } + return NULL; +} + static apr_status_t duration_parse(const char *value, apr_interval_time_t *ptimeout, const char *def_unit) { @@ -516,6 +572,10 @@ static const char *md_config_set_proxy(cmd_parms *cmd, void *arg, const char *va md_srv_conf_t *sc = md_config_get(cmd->server); const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); + if (err) { + return err; + } + md_util_abs_http_uri_check(cmd->pool, value, &err); if (err) { return err; } @@ -690,6 +750,8 @@ const command_rec md_cmds[] = { AP_INIT_TAKE_ARGV( MD_CMD_MEMBERS, md_config_sec_add_members, NULL, RSRC_CONF, "Define domain name(s) part of the Managed Domain. Use 'auto' or " "'manual' to enable/disable auto adding names from virtual hosts."), + AP_INIT_TAKE1( MD_CMD_MUST_STAPLE, md_config_set_must_staple, NULL, RSRC_CONF, + "Enable/Disable the Must-Staple flag for new certificates."), AP_INIT_TAKE12( MD_CMD_PORTMAP, md_config_set_port_map, NULL, RSRC_CONF, "Declare the mapped ports 80 and 443 on the local server. E.g. 80:8000 " "to indicate that the server port 8000 is reachable as port 80 from the " @@ -703,6 +765,8 @@ const command_rec md_cmds[] = { "the directory for file system storage of managed domain data."), AP_INIT_TAKE1( MD_CMD_RENEWWINDOW, md_config_set_renew_window, NULL, RSRC_CONF, "Time length for renewal before certificate expires (defaults to days)"), + AP_INIT_TAKE1( MD_CMD_REQUIREHTTPS, md_config_set_require_https, NULL, RSRC_CONF, + "Redirect non-secure requests to the https: equivalent."), AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL) }; @@ -765,6 +829,10 @@ int md_config_geti(const md_srv_conf_t *sc, md_config_var_t var) return sc->mc->local_443; case MD_CONFIG_TRANSITIVE: return (sc->transitive != DEF_VAL)? sc->transitive : defconf.transitive; + case MD_CONFIG_REQUIRE_HTTPS: + return (sc->require_https != MD_REQUIRE_UNSET)? sc->require_https : defconf.require_https; + case MD_CONFIG_MUST_STAPLE: + return (sc->must_staple != DEF_VAL)? sc->must_staple : defconf.must_staple; default: return 0; } diff --git a/modules/md/mod_md_config.h b/modules/md/mod_md_config.h index b385509476..99593c35bc 100644 --- a/modules/md/mod_md_config.h +++ b/modules/md/mod_md_config.h @@ -31,6 +31,8 @@ typedef enum { MD_CONFIG_RENEW_WINDOW, MD_CONFIG_TRANSITIVE, MD_CONFIG_PROXY, + MD_CONFIG_REQUIRE_HTTPS, + MD_CONFIG_MUST_STAPLE, } md_config_var_t; typedef struct { @@ -53,6 +55,7 @@ typedef struct md_srv_conf_t { md_mod_conf_t *mc; /* global config settings */ int transitive; /* != 0 iff VirtualHost names/aliases are auto-added */ + md_require_t require_https; /* If MDs require https: access */ int drive_mode; /* mode of obtaining credentials */ int must_staple; /* certificates should set the OCSP Must Staple extension */ struct md_pkey_spec_t *pkey_spec; /* specification for generating private keys */ -- 2.40.0