From 7019a88e61af97e947e251840b4e37fbd8b7b8b5 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Fri, 15 Aug 2025 11:23:29 +0000 Subject: [PATCH] *) mod_md: update to version 2.6.1 - Increasing default `MDRetryDelay` to 30 seconds to generate less bursty traffic on errored renewals for the ACME CA. This leads to error retries of 30s, 1 minute, 2, 4, etc. up to daily attempts. - Checking that configuring `MDRetryDelay` will result in a positive duration. A delay of 0 is not accepted. - Fix a bug in checking Content-Type of responses from the ACME server. - Added ACME ARI support (rfc9773) to the module. Enabled by default. New directive "MDRenewViaARI on|off" for controlling this. - Removing tailscale support. It has not been working for a long time as the company decided to change their APIs. Away with the dead code, documentation and tests. - Fixed a compilation issue with pre-industrial versions of libcurl. git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1927807 13f79535-47bb-0310-9956-ffa450edef68 --- changes-entries/md_v2.6.1.txt | 13 + docs/manual/mod/mod_md.xml | 28 +- modules/md/config2.m4 | 1 - modules/md/md.h | 3 + modules/md/md_acme.c | 18 +- modules/md/md_acme.h | 4 +- modules/md/md_acme_authz.c | 2 +- modules/md/md_acme_drive.c | 153 +++++++++- modules/md/md_acme_order.c | 24 +- modules/md/md_acme_order.h | 2 +- modules/md/md_acmev2_drive.c | 35 ++- modules/md/md_core.c | 4 + modules/md/md_crypt.c | 84 +++++- modules/md/md_crypt.h | 7 + modules/md/md_http.c | 12 +- modules/md/md_json.c | 10 +- modules/md/md_reg.c | 76 ++++- modules/md/md_reg.h | 11 + modules/md/md_status.c | 48 +++- modules/md/md_tailscale.c | 383 -------------------------- modules/md/md_tailscale.h | 25 -- modules/md/md_time.c | 67 +++++ modules/md/md_time.h | 3 + modules/md/md_version.h | 5 +- modules/md/mod_md.c | 6 +- modules/md/mod_md.dsp | 4 - modules/md/mod_md_config.c | 27 +- modules/md/mod_md_config.h | 2 + modules/md/mod_md_drive.c | 77 +++++- modules/md/mod_md_status.c | 6 +- test/modules/md/md_conf.py | 2 + test/modules/md/test_702_auto.py | 3 +- test/modules/md/test_710_profiles.py | 2 +- test/modules/md/test_730_static.py | 10 + test/modules/md/test_780_tailscale.py | 198 ------------- test/modules/md/test_920_status.py | 5 + 36 files changed, 660 insertions(+), 700 deletions(-) create mode 100644 changes-entries/md_v2.6.1.txt delete mode 100644 modules/md/md_tailscale.c delete mode 100644 modules/md/md_tailscale.h delete mode 100644 test/modules/md/test_780_tailscale.py diff --git a/changes-entries/md_v2.6.1.txt b/changes-entries/md_v2.6.1.txt new file mode 100644 index 00000000000..f3d1c30f8e3 --- /dev/null +++ b/changes-entries/md_v2.6.1.txt @@ -0,0 +1,13 @@ + *) mod_md: update to version 2.6.1 + - Increasing default `MDRetryDelay` to 30 seconds to generate less bursty + traffic on errored renewals for the ACME CA. This leads to error retries + of 30s, 1 minute, 2, 4, etc. up to daily attempts. + - Checking that configuring `MDRetryDelay` will result in a positive + duration. A delay of 0 is not accepted. + - Fix a bug in checking Content-Type of responses from the ACME server. + - Added ACME ARI support (rfc9773) to the module. Enabled by default. New + directive "MDRenewViaARI on|off" for controlling this. + - Removing tailscale support. It has not been working for a long time + as the company decided to change their APIs. Away with the dead code, + documentation and tests. + - Fixed a compilation issue with pre-industrial versions of libcurl. diff --git a/docs/manual/mod/mod_md.xml b/docs/manual/mod/mod_md.xml index 09bae08a3bb..c383294c0da 100644 --- a/docs/manual/mod/mod_md.xml +++ b/docs/manual/mod/mod_md.xml @@ -1393,7 +1393,7 @@ MDMessageCmd /etc/apache/md-message MDRetryDelay Time length for first retry, doubled on every consecutive error. MDRetryDelay duration - MDRetryDelay 5s + MDRetryDelay 30s server config @@ -1408,6 +1408,10 @@ MDMessageCmd /etc/apache/md-message It is kept separate for each certificate renewal. Meaning an error on one MDomain does not delay the renewals of other domains.

+

+ In mod_md v2.6.1, the default delay has been increased from 5 + seconds to 30. +

@@ -1594,4 +1598,26 @@ MDMessageCmd /etc/apache/md-message

+ + + MDRenewViaARI + usage of the ACME ARI extension (rfc9773). + MDRenewViaARI on|off + MDRenewViaARI on + + server config + + +

+ En-/Disable certificate renewals triggered via the ACME ARI + extension (rfc9773). These renewals happen *in addition* to + the mechanism controlled by MDRenewWindow. +

+ ACME ARI allows an ACME CA to somewhat shape incoming renewal + traffic. More importantly though, it can inform clients of + urgent renewals, e.g. when a certificate or part of its chain + has been revoked. +

+
+
diff --git a/modules/md/config2.m4 b/modules/md/config2.m4 index e416736a8c1..b20ab3b45e1 100644 --- a/modules/md/config2.m4 +++ b/modules/md/config2.m4 @@ -156,7 +156,6 @@ 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 diff --git a/modules/md/md.h b/modules/md/md.h index 3f298eaa6f3..fb1a270ac8f 100644 --- a/modules/md/md.h +++ b/modules/md/md.h @@ -94,6 +94,7 @@ struct md_t { const char *ca_eab_hmac; /* optional HMAC for external account binding */ const char *profile; /* optional cert profile to order */ int profile_mandatory; /* if profile, when given, is mandatory */ + int ari_renewals; /* if ACME ARI (RFC 9773) can trigger renewals */ const char *state_descr; /* description of state of NULL */ @@ -119,6 +120,8 @@ struct md_t { #define MD_KEY_ACTIVATION_DELAY "activation-delay" #define MD_KEY_ACTIVITY "activity" #define MD_KEY_AGREEMENT "agreement" +#define MD_KEY_ARI_CERT_ID "ari-cert-id" +#define MD_KEY_ARI_RENEWALS "ari-renewals" #define MD_KEY_AUTHORIZATIONS "authorizations" #define MD_KEY_BITS "bits" #define MD_KEY_CA "ca" diff --git a/modules/md/md_acme.c b/modules/md/md_acme.c index f8624513ba8..33a7afa310a 100644 --- a/modules/md/md_acme.c +++ b/modules/md/md_acme.c @@ -332,7 +332,7 @@ static apr_status_t acmev2_GET_as_POST_init(md_acme_req_t *req, void *baton) return md_acme_req_body_init(req, NULL); } -static apr_status_t md_acme_req_send(md_acme_req_t *req) +static apr_status_t md_acme_req_send(md_acme_req_t *req, int get_as_post) { apr_status_t rv; md_acme_t *acme = req->acme; @@ -352,7 +352,7 @@ static apr_status_t md_acme_req_send(md_acme_req_t *req) if (APR_SUCCESS != rv) goto leave; } - if (!strcmp("GET", req->method) && !req->on_init && !req->req_json) { + if (get_as_post && !strcmp("GET", req->method) && !req->on_init && !req->req_json) { /* See * and * and @@ -420,7 +420,7 @@ static apr_status_t md_acme_req_send(md_acme_req_t *req) if (APR_EAGAIN == rv && req->max_retries > 0) { --req->max_retries; - rv = md_acme_req_send(req); + rv = md_acme_req_send(req, 1); } req = NULL; @@ -449,14 +449,15 @@ apr_status_t md_acme_POST(md_acme_t *acme, const char *url, req->on_err = on_err; req->baton = baton; - return md_acme_req_send(req); + return md_acme_req_send(req, 1); } 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, + md_acme_req_err_cb *on_err, + int get_as_post, void *baton) { md_acme_req_t *req; @@ -472,7 +473,7 @@ apr_status_t md_acme_GET(md_acme_t *acme, const char *url, req->on_err = on_err; req->baton = baton; - return md_acme_req_send(req); + return md_acme_req_send(req, get_as_post); } void md_acme_report_result(md_acme_t *acme, apr_status_t rv, struct md_result_t *result) @@ -507,7 +508,7 @@ static apr_status_t on_got_json(md_acme_t *acme, apr_pool_t *p, const apr_table_ } apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme, - const char *url, apr_pool_t *p) + const char *url, int get_as_post, apr_pool_t *p) { apr_status_t rv; json_ctx ctx; @@ -515,7 +516,7 @@ 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, NULL, &ctx); + rv = md_acme_GET(acme, url, NULL, on_got_json, NULL, NULL, get_as_post, &ctx); *pjson = (APR_SUCCESS == rv)? ctx.json : NULL; return rv; } @@ -720,6 +721,7 @@ static apr_status_t update_directory(const md_http_response_t *res, void *data) 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); + acme->api.v2.renewal_info = md_json_dups(acme->p, json, "renewalInfo", 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" diff --git a/modules/md/md_acme.h b/modules/md/md_acme.h index 9931f92493f..c2b98b412d0 100644 --- a/modules/md/md_acme.h +++ b/modules/md/md_acme.h @@ -118,6 +118,7 @@ struct md_acme_t { const char *key_change; const char *revoke_cert; const char *new_nonce; + const char *renewal_info; struct apr_array_header_t *profiles; } v2; } api; @@ -275,6 +276,7 @@ apr_status_t md_acme_GET(md_acme_t *acme, const char *url, md_acme_req_json_cb *on_json, md_acme_req_res_cb *on_res, md_acme_req_err_cb *on_err, + int get_as_post, void *baton); /** * Perform a POST against the ACME url. If a on_json callback is given and @@ -301,7 +303,7 @@ apr_status_t md_acme_POST(md_acme_t *acme, const char *url, * Retrieve a JSON resource from the ACME server */ apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme, - const char *url, apr_pool_t *p); + const char *url, int get_as_post, apr_pool_t *p); apr_status_t md_acme_req_body_init(md_acme_req_t *req, struct md_json_t *jpayload); diff --git a/modules/md/md_acme_authz.c b/modules/md/md_acme_authz.c index fc46274fffd..c77dcdbaeb3 100644 --- a/modules/md/md_acme_authz.c +++ b/modules/md/md_acme_authz.c @@ -131,7 +131,7 @@ apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, apr_p err = "unable to parse response"; log_level = MD_LOG_ERR; - if (APR_SUCCESS == (rv = md_acme_get_json(&json, acme, authz->url, p)) + if (APR_SUCCESS == (rv = md_acme_get_json(&json, acme, authz->url, 1, p)) && (s = md_json_gets(json, MD_KEY_STATUS, NULL))) { authz->domain = md_json_gets(json, MD_KEY_IDENTIFIER, MD_KEY_VALUE, NULL); diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c index f5cd08c5214..94bcc8aab47 100644 --- a/modules/md/md_acme_drive.c +++ b/modules/md/md_acme_drive.c @@ -256,7 +256,7 @@ static apr_status_t get_cert(void *baton, int attempt) (void)attempt; 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); + return md_acme_GET(ad->acme, ad->order->certificate, NULL, NULL, on_add_cert, NULL, 1, d); } apr_status_t md_acme_drive_cert_poll(md_proto_driver_t *d, int only_once) @@ -429,7 +429,7 @@ static apr_status_t get_chain(void *baton, int attempt) md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "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); + rv = md_acme_GET(ad->acme, ad->chain_up_link, NULL, NULL, on_add_chain, NULL, 1, d); if (APR_SUCCESS == rv && nelts == ad->cred->chain->nelts) { break; @@ -1094,10 +1094,155 @@ static apr_status_t acme_complete_md(md_t *md, apr_pool_t *p) return APR_SUCCESS; } +static apr_status_t acme_get_ari(md_proto_driver_t *d, + struct md_result_t *result, + apr_time_t *prenew_at, + const char **purl) +{ + md_acme_driver_t *ad = d->baton; + apr_status_t rv = APR_SUCCESS; + const char *ca_effective = NULL; + apr_array_header_t *certs; + const md_cert_t *cert; + int i; + + *prenew_at = 0; + *purl = NULL; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "get ARI status for %s", d->md->name); + + if (d->md->cert_files && d->md->cert_files->nelts) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "%s is configured with static files, no ARI", d->md->name); + goto out; + } + + 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_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "%s is missing MDCertificateAuthority", d->md->name); + goto out; + } + + /* always pick the first CA for this */ + ca_effective = APR_ARRAY_IDX(d->md->ca_urls, 0, const char*); + + certs = apr_array_make(d->p, 5, sizeof(md_cert_t*)); + for (i = 0; i < md_pkeys_spec_count(d->md->pks); ++i) { + const md_pubcert_t *pubcert; + if (APR_SUCCESS == md_reg_get_pubcert(&pubcert, d->reg, d->md, i, d->p)) { + cert = APR_ARRAY_IDX(pubcert->certs, 0, const md_cert_t*); + APR_ARRAY_PUSH(certs, const md_cert_t*) = cert; + } + } + + if (!certs->nelts) { + rv = APR_SUCCESS; + goto out; + } + + if (APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, ca_effective, + d->proxy_url, d->ca_file))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "create ACME communications"); + goto out; + } + if (APR_SUCCESS != (rv = md_acme_setup(ad->acme, result))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "setup ACME communications"); + goto out; + } + if (ad->acme->version != MD_ACME_VERSION_2 || !ad->acme->api.v2.renewal_info) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "ARI not supported by ACME CA %s", ca_effective); + goto out; + } + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "assessing ARI status for %d certificates", certs->nelts); + for (i = 0; i < certs->nelts; ++i) { + const char *ari_cert_id; + const char *ari_url = NULL, *ari_expl_url; + const char *renew_start, *renew_end; + apr_time_t start, end; + md_json_t *json; + unsigned char c; + + cert = APR_ARRAY_IDX(certs, i, md_cert_t*); + if (md_cert_get_ari_cert_id(&ari_cert_id, cert, d->p) != APR_SUCCESS) + continue; + + ari_url = apr_psprintf(d->p, "%s/%s", ad->acme->api.v2.renewal_info, + ari_cert_id); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "GET #%d ARI from %s", i, ari_url); + if ((rv = md_acme_get_json(&json, ad->acme, ari_url, 0, d->p)) != APR_SUCCESS) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, d->p, + "error retrieving ARI from %s", ari_url); + continue; + } + + if(!json) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, d->p, + "ARI returned no JSON from %s", ari_url); + continue; + } + + renew_start = md_json_gets(json, "suggestedWindow", "start", NULL); + renew_end = md_json_gets(json, "suggestedWindow", "end", NULL); + ari_expl_url = md_json_gets(json, "explanationURL", NULL); + if (!renew_start || !renew_end) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, d->p, + "renewal info from CA incomplete for %s", ari_url); + continue; + } + start = md_time_parse_rfc3339(renew_start); + end = md_time_parse_rfc3339(renew_end); + if (!start) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, d->p, + "error parsing CA renew start time: '%s'", + renew_start); + continue; + } + if (!end) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, d->p, + "error parsing CA renew end time: '%s'", + renew_end); + continue; + } + if (start > end) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, d->p, + "CA advises weird renewal between '%s' and '%s'", + renew_start, renew_end); + continue; + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "CA advises renew via ARI between %s and %s" + " (explantion: %s)", + renew_start, renew_end, + ari_expl_url? ari_expl_url : "none given"); + /* select a random value between start and end */ + md_rand_bytes(&c, sizeof(c), d->p); + start += apr_time_from_sec((apr_time_sec(end - start) * (c - 128)) / 256); + if (!*prenew_at || (start < *prenew_at)) { + *prenew_at = start; + *purl = apr_pstrdup(d->p, ari_expl_url); + } + } + + rv = APR_SUCCESS; +out: + return rv; +} + static md_proto_t ACME_PROTO = { - MD_PROTO_ACME, acme_driver_init, acme_driver_renew, - acme_driver_preload_init, acme_driver_preload, + MD_PROTO_ACME, + acme_driver_init, + acme_driver_renew, + acme_driver_preload_init, + acme_driver_preload, acme_complete_md, + acme_get_ari, }; apr_status_t md_acme_protos_add(apr_hash_t *protos, apr_pool_t *p) diff --git a/modules/md/md_acme_order.c b/modules/md/md_acme_order.c index 0a0ad7ff0ae..22d84a2594a 100644 --- a/modules/md/md_acme_order.c +++ b/modules/md/md_acme_order.c @@ -264,13 +264,15 @@ typedef struct { md_acme_t *acme; const char *name; const char *profile; + const char *ari_cert_id; apr_array_header_t *domains; md_result_t *result; } order_ctx_t; -#define ORDER_CTX_INIT(ctx, p, o, a, n, d, pf, r) \ +#define ORDER_CTX_INIT(ctx, p, o, a, n, d, pf, cid, r) \ (ctx)->p = (p); (ctx)->order = (o); (ctx)->acme = (a); \ - (ctx)->name = (n); (ctx)->domains = d; (ctx)->profile = pf; (ctx)->result = r + (ctx)->name = (n); (ctx)->domains = d; (ctx)->profile = pf; \ + (ctx)->ari_cert_id = cid;(ctx)->result = r static apr_status_t identifier_to_json(void *value, md_json_t *json, apr_pool_t *p, void *baton) { @@ -291,7 +293,9 @@ static apr_status_t on_init_order_register(md_acme_req_t *req, void *baton) jpayload = md_json_create(req->p); md_json_seta(ctx->domains, identifier_to_json, NULL, jpayload, "identifiers", NULL); if (ctx->profile) - md_json_sets(ctx->profile, jpayload, "profile", NULL); + md_json_sets(ctx->profile, jpayload, "profile", NULL); + if (ctx->ari_cert_id) + md_json_sets(ctx->ari_cert_id, jpayload, "replaces", NULL); return md_acme_req_body_init(req, jpayload); } @@ -325,13 +329,13 @@ out: 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, - const char *profile) + const char *profile, const char *ari_cert_id) { 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, profile, NULL); + ORDER_CTX_INIT(&ctx, p, NULL, acme, name, domains, profile, ari_cert_id, 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; @@ -344,8 +348,8 @@ apr_status_t md_acme_order_update(md_acme_order_t *order, md_acme_t *acme, apr_status_t rv; assert(MD_ACME_VERSION_MAJOR(acme->version) > 1); - ORDER_CTX_INIT(&ctx, p, order, acme, NULL, NULL, NULL, result); - rv = md_acme_GET(acme, order->url, NULL, on_order_upd, NULL, NULL, &ctx); + ORDER_CTX_INIT(&ctx, p, order, acme, NULL, NULL, NULL, NULL, result); + rv = md_acme_GET(acme, order->url, NULL, on_order_upd, NULL, NULL, 1, &ctx); if (APR_SUCCESS != rv && APR_SUCCESS != acme->last->status) { md_result_dup(result, acme->last); } @@ -384,7 +388,7 @@ apr_status_t md_acme_order_await_ready(md_acme_order_t *order, md_acme_t *acme, apr_status_t rv; assert(MD_ACME_VERSION_MAJOR(acme->version) > 1); - ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, NULL, result); + ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, NULL, 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); @@ -427,7 +431,7 @@ apr_status_t md_acme_order_await_valid(md_acme_order_t *order, md_acme_t *acme, apr_status_t rv; assert(MD_ACME_VERSION_MAJOR(acme->version) > 1); - ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, NULL, result); + ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, NULL, 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); @@ -556,7 +560,7 @@ apr_status_t md_acme_order_monitor_authzs(md_acme_order_t *order, md_acme_t *acm order_ctx_t ctx; apr_status_t rv; - ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, NULL, result); + ORDER_CTX_INIT(&ctx, p, order, acme, md->name, NULL, NULL, 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); diff --git a/modules/md/md_acme_order.h b/modules/md/md_acme_order.h index 01d73d41b9f..2d08cd9861a 100644 --- a/modules/md/md_acme_order.h +++ b/modules/md/md_acme_order.h @@ -77,7 +77,7 @@ apr_status_t md_acme_order_monitor_authzs(md_acme_order_t *order, md_acme_t *acm 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, - const char *profile); + const char *profile, const char *ari_cert_id); 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); diff --git a/modules/md/md_acmev2_drive.c b/modules/md/md_acmev2_drive.c index e5821e560ae..de58c7247ca 100644 --- a/modules/md/md_acmev2_drive.c +++ b/modules/md/md_acmev2_drive.c @@ -57,7 +57,8 @@ static apr_status_t ad_setup_order(md_proto_driver_t *d, md_result_t *result, in apr_status_t rv; md_t *md = ad->md; const char *profile = NULL; - + const char *ari_cert_id = NULL; + assert(ad->md); assert(ad->acme); @@ -77,9 +78,34 @@ static apr_status_t ad_setup_order(md_proto_driver_t *d, md_result_t *result, in md_acme_order_purge(d->store, d->p, MD_SG_STAGING, md, d->env); } - md_result_activity_setn(result, "Creating new order"); + if (ad->cred->spec && ad->md->ca_account) { + /* are we replacing a previous certificate on the same account? */ + int i; + for (i = 0; i < md_pkeys_spec_count(d->md->pks); ++i) { + md_pkey_spec_t *spec = md_pkeys_spec_get(d->md->pks, i); + const md_pubcert_t *pubcert; + const md_cert_t *cert; + if (md_pkey_spec_eq(ad->cred->spec, spec)) { + rv = md_reg_get_pubcert(&pubcert, d->reg, d->md, i, d->p); + if (rv == APR_SUCCESS) { + cert = APR_ARRAY_IDX(pubcert->certs, 0, const md_cert_t*); + if (cert) { + md_cert_get_ari_cert_id(&ari_cert_id, cert, d->p); + break; + } + } + } + } + } + + md_result_activity_printf(result, "Creating new order, key-spec=%s, " + "profile=%s, replacing-cert=%s", + ad->cred->spec? md_pkey_spec_to_str(ad->cred->spec, d->p) : "default", + ad->profile? ad->profile : "none", + ari_cert_id? ari_cert_id : "none"); + if (ad->profile) { - if(ad->acme->api.v2.profiles) { + if (ad->acme->api.v2.profiles) { int i; for (i = 0; !profile && i < ad->acme->api.v2.profiles->nelts; ++i) { const char *s = APR_ARRAY_IDX(ad->acme->api.v2.profiles, i, const char*); @@ -104,7 +130,8 @@ static apr_status_t ad_setup_order(md_proto_driver_t *d, md_result_t *result, in } } - rv = md_acme_order_register(&ad->order, ad->acme, d->p, d->md->name, ad->domains, profile); + rv = md_acme_order_register(&ad->order, ad->acme, d->p, d->md->name, + ad->domains, profile, ari_cert_id); 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) { diff --git a/modules/md/md_core.c b/modules/md/md_core.c index 70f20c4bebe..95b289d2d48 100644 --- a/modules/md/md_core.c +++ b/modules/md/md_core.c @@ -112,6 +112,8 @@ md_t *md_create_empty(apr_pool_t *p) md->transitive = -1; md->acme_tls_1_domains = apr_array_make(p, 5, sizeof(const char *)); md->stapling = -1; + md->profile_mandatory = -1; + md->ari_renewals = -1; md->defn_name = "unknown"; md->defn_line_number = 0; } @@ -319,6 +321,7 @@ md_json_t *md_to_json(const md_t *md, apr_pool_t *p) } if (md->profile) md_json_sets(md->profile, json, MD_KEY_PROFILE, NULL); md_json_setb(md->profile_mandatory > 0, json, MD_KEY_PROFILE_MANDATORY, NULL); + md_json_setb(md->ari_renewals > 0, json, MD_KEY_ARI_RENEWALS, NULL); return json; } return NULL; @@ -389,6 +392,7 @@ md_t *md_from_json(md_json_t *json, apr_pool_t *p) md->profile_mandatory = (int)md_json_getb(json, MD_KEY_PROFILE_MANDATORY, NULL); if (md_json_has_key(json, MD_KEY_PROFILE, NULL)) md->profile = md_json_dups(p, json, MD_KEY_PROFILE, NULL); + md->ari_renewals = (int)md_json_getb(json, MD_KEY_ARI_RENEWALS, NULL); return md; } return NULL; diff --git a/modules/md/md_crypt.c b/modules/md/md_crypt.c index e56a2c0c9b7..c5a6dd6f9eb 100644 --- a/modules/md/md_crypt.c +++ b/modules/md/md_crypt.c @@ -353,6 +353,24 @@ void md_pkeys_spec_add_ec(md_pkeys_spec_t *pks, const char *curve) md_pkeys_spec_add(pks, spec); } +const char *md_pkey_spec_to_str(const md_pkey_spec_t *spec, apr_pool_t *p) +{ + switch (spec->type) { + case MD_PKEY_TYPE_DEFAULT: + return "default"; + case MD_PKEY_TYPE_RSA: + if (spec->params.rsa.bits >= MD_PKEY_RSA_BITS_MIN) + return apr_psprintf(p, "rsa-%d", spec->params.rsa.bits); + return "rsa"; + case MD_PKEY_TYPE_EC: + if (spec->params.ec.curve) + return apr_psprintf(p, "ec-%s", spec->params.ec.curve); + return "ec"; + default: + return "unsupported"; + } +} + 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); @@ -460,7 +478,7 @@ md_pkeys_spec_t *md_pkeys_spec_from_json(struct md_json_t *json, apr_pool_t *p) return pks; } -static int pkey_spec_eq(md_pkey_spec_t *s1, md_pkey_spec_t *s2) +int md_pkey_spec_eq(const md_pkey_spec_t *s1, const md_pkey_spec_t *s2) { if (s1 == s2) { return 1; @@ -496,8 +514,8 @@ int md_pkeys_spec_eq(md_pkeys_spec_t *pks1, md_pkeys_spec_t *pks2) } 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 *))) { + if (!md_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; } } @@ -1302,7 +1320,7 @@ int md_cert_covers_md(md_cert_t *cert, const md_t *md) const char *md_cert_get_issuer_name(const md_cert_t *cert, apr_pool_t *p) { X509_NAME *xname = X509_get_issuer_name(cert->x509); - if(xname) { + if (xname) { char *name, *s = X509_NAME_oneline(xname, NULL, 0); name = apr_pstrdup(p, s); OPENSSL_free(s); @@ -2193,3 +2211,61 @@ apr_status_t md_check_cert_and_pkey(struct apr_array_header_t *certs, md_pkey_t return APR_SUCCESS; } + +apr_status_t md_cert_get_ari_cert_id(const char **pari_cert_id, + const md_cert_t *cert, apr_pool_t *p) +{ +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + md_data_t akid_buf, ser_buf; + AUTHORITY_KEYID *s_aki; + const ASN1_INTEGER *aki; + const ASN1_INTEGER *serial; + BIGNUM *bn; + int i = -1, sder_len; + unsigned char *ucp, sbuf[256]; + + *pari_cert_id = NULL; + s_aki = X509_get_ext_d2i(cert->x509, NID_authority_key_identifier, &i, NULL); + if (s_aki == NULL) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "cert has no authority key id extension"); + return APR_ENOENT; + } + aki = s_aki->keyid; + if (aki == NULL) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "cert has no authority key id in extension"); + return APR_ENOENT; + } + akid_buf.len = (apr_size_t)ASN1_STRING_length(aki); + akid_buf.data = (const char *)ASN1_STRING_get0_data(aki); + akid_buf.free_data = NULL; + + serial = X509_get0_serialNumber(cert->x509); + if (!serial) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "cert has no serial number"); + return APR_ENOENT; + } + memset(&ser_buf, 0, sizeof(ser_buf)); + bn = ASN1_INTEGER_to_BN(serial, NULL); + sder_len = BN_bn2bin(bn, sbuf); + OPENSSL_free((void*)bn); + if (sder_len < 1) + return APR_EINVAL; + ser_buf.len = (apr_size_t)sder_len; + ser_buf.data = (const char *)sbuf; + (void)ucp; + + *pari_cert_id = apr_psprintf(p, "%s.%s", + md_util_base64url_encode(&akid_buf, p), + md_util_base64url_encode(&ser_buf, p)); + return APR_SUCCESS; +#else + *pari_cert_id = NULL; + (void)cert; + (void)p; + return APR_ENOTIMPL; +#endif +} + diff --git a/modules/md/md_crypt.h b/modules/md/md_crypt.h index e6b3ac2e783..6880cd2c095 100644 --- a/modules/md/md_crypt.h +++ b/modules/md/md_crypt.h @@ -68,6 +68,8 @@ typedef struct md_pkey_spec_t { } params; } md_pkey_spec_t; +int md_pkey_spec_eq(const md_pkey_spec_t *s1, const md_pkey_spec_t *s2); + typedef struct md_pkeys_spec_t { apr_pool_t *p; struct apr_array_header_t *specs; @@ -90,6 +92,8 @@ 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); +const char *md_pkey_spec_to_str(const md_pkey_spec_t *spec, apr_pool_t *p); + 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); @@ -235,6 +239,9 @@ apr_status_t md_cert_get_ocsp_responder_url(const char **purl, apr_pool_t *p, co apr_status_t md_check_cert_and_pkey(struct apr_array_header_t *certs, md_pkey_t *pkey); +apr_status_t md_cert_get_ari_cert_id(const char **pari_cert_id, + const md_cert_t *cert, apr_pool_t *p); + /**************************************************************************************************/ /* X509 certificate transparency */ diff --git a/modules/md/md_http.c b/modules/md/md_http.c index 0d21e7b14c6..283f4be1352 100644 --- a/modules/md/md_http.c +++ b/modules/md/md_http.c @@ -242,11 +242,13 @@ static apr_status_t req_create(md_http_request_t **preq, md_http_t *http, void md_http_req_destroy(md_http_request_t *req) { - if (req->internals) { - req->http->impl->req_cleanup(req); - req->internals = NULL; + if (req) { + if (req->internals) { + req->http->impl->req_cleanup(req); + req->internals = NULL; + } + apr_pool_destroy(req->pool); } - apr_pool_destroy(req->pool); } void md_http_set_on_status_cb(md_http_request_t *req, md_http_status_cb *cb, void *baton) @@ -341,7 +343,7 @@ cleanup: } else { *preq = NULL; - if (req) md_http_req_destroy(req); + md_http_req_destroy(req); } return rv; } diff --git a/modules/md/md_json.c b/modules/md/md_json.c index e0f977ea564..bd0e1c5a3b7 100644 --- a/modules/md/md_json.c +++ b/modules/md/md_json.c @@ -1186,14 +1186,18 @@ apr_status_t md_json_read_http(md_json_t **pjson, apr_pool_t *pool, const md_htt { apr_status_t rv = APR_ENOENT; const char *ctype, *p; + apr_size_t ctype_len; *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")) { + ctype_len = strlen(ctype); + if (ctype_len < sizeof("/json")) goto cleanup; + p = ctype + ctype_len + 1; /* point to the terminating 0 */ + if (!strcmp(p - sizeof("/json"), "/json") || + !strcmp(p - sizeof("+json"), "+json") || + !strcmp(ctype, "text/plain")) { rv = md_json_readb(pjson, pool, res->body); } cleanup: diff --git a/modules/md/md_reg.c b/modules/md/md_reg.c index d0a41de177d..5da2f6dd4bc 100644 --- a/modules/md/md_reg.c +++ b/modules/md/md_reg.c @@ -34,7 +34,6 @@ #include "md_ocsp.h" #include "md_store.h" #include "md_status.h" -#include "md_tailscale.h" #include "md_util.h" #include "md_acme.h" @@ -122,8 +121,7 @@ apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *p, struct md_store_t *st 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)) - && APR_SUCCESS == (rv = md_tailscale_protos_add(reg->protos, p))) { + if (APR_SUCCESS == (rv = md_acme_protos_add(reg->protos, p))) { rv = load_props(reg, p); } @@ -239,9 +237,9 @@ static apr_status_t state_init(md_reg_t *reg, apr_pool_t *p, md_t *md) if (md->renew_window == NULL) md->renew_window = reg->renew_window; if (md->warn_window == NULL) md->warn_window = reg->warn_window; - if(is_static) { - if(md->renew_mode == MD_RENEW_AUTO) - md->renew_mode = MD_RENEW_MANUAL; + if (is_static) { + if (md->renew_mode == MD_RENEW_AUTO) + md->renew_mode = MD_RENEW_MANUAL; } if (md->domains && md->domains->pool != p) { @@ -691,13 +689,18 @@ apr_time_t md_reg_renew_at(md_reg_t *reg, const md_t *md, apr_pool_t *p) const md_cert_t *cert; md_timeperiod_t certlife, renewal; int i; - apr_time_t renew_at = 0; + apr_time_t renew_at = 0, now = apr_time_now(); apr_status_t rv; - if (md->state == MD_S_INCOMPLETE) return apr_time_now(); + if (md->state == MD_S_INCOMPLETE) return 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_STATUS_IS_ENOENT(rv)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, + "md(%s): is missing certificate #%d", + md->name, i); + return now; + } if (APR_SUCCESS == rv) { cert = APR_ARRAY_IDX(pub->certs, 0, const md_cert_t*); certlife.start = md_cert_get_not_before(cert); @@ -705,7 +708,7 @@ apr_time_t md_reg_renew_at(md_reg_t *reg, const md_t *md, apr_pool_t *p) 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_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "md[%s]: certificate(%d) valid[%s] renewal[%s]", md->name, i, md_timeperiod_print(p, &certlife), @@ -713,7 +716,7 @@ apr_time_t md_reg_renew_at(md_reg_t *reg, const md_t *md, apr_pool_t *p) } if (renew_at == 0 || renewal.start < renew_at) { - renew_at = renewal.start; + renew_at = renewal.start; } } } @@ -1002,6 +1005,8 @@ apr_status_t md_reg_sync_finish(md_reg_t *reg, md_t *md, apr_pool_t *p, apr_pool && !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_VAL_UPDATE(md, old, profile_mandatory) + && !MD_VAL_UPDATE(md, old, ari_renewals) && 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) @@ -1201,6 +1206,53 @@ apr_status_t md_reg_renew(md_reg_t *reg, const md_t *md, apr_table_t *env, return md_util_pool_vdo(run_renew, reg, p, md, env, reset, attempt, result, NULL); } +static apr_status_t run_get_ari(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) +{ + md_reg_t *reg = baton; + apr_time_t *prenew_at = 0; + const char **purl; + const md_t *md; + md_proto_driver_t *driver; + apr_table_t *env; + apr_status_t rv; + md_result_t *result; + + (void)p; + prenew_at = va_arg(ap, apr_time_t *); + purl = va_arg(ap, const char **); + md = va_arg(ap, const md_t *); + env = va_arg(ap, apr_table_t *); + result = va_arg(ap, md_result_t *); + + rv = run_init(reg, ptemp, &driver, md, 0, env, result, NULL); + if (APR_SUCCESS == rv) { + if (driver->proto->get_ari) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "%s: run get_ari", + md->name); + rv = driver->proto->get_ari(driver, result, prenew_at, purl); + } + else { + /* unsupported by protocol */ + *prenew_at = 0; + *purl = NULL; + rv = APR_ENOTIMPL; + } + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "%s: get_ari done", md->name); + return rv; +} + +apr_time_t md_reg_ari_renew_at(const char **purl, md_reg_t *reg, + const md_t *md, struct apr_table_t *env, + struct md_result_t *result, apr_pool_t *p) +{ + apr_time_t renew_at = 0; + *purl = NULL; + if (md_util_pool_vdo(run_get_ari, reg, p, &renew_at, purl, md, env, result, NULL) == APR_SUCCESS) + return renew_at; + return 0; +} + 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; @@ -1379,7 +1431,7 @@ int md_reg_has_revoked_certs(md_reg_t *reg, struct md_ocsp_reg_t *ocsp, if (APR_SUCCESS != md_reg_get_pubcert(&pubcert, reg, md, i, p)) continue; cert = APR_ARRAY_IDX(pubcert->certs, 0, const md_cert_t*); - if(!cert) + if (!cert) continue; rv = md_ocsp_get_meta(&cert_stat, &ocsp_valid, ocsp, cert, p, md); if (APR_SUCCESS == rv && cert_stat == MD_OCSP_CERT_ST_REVOKED) { diff --git a/modules/md/md_reg.h b/modules/md/md_reg.h index 191b026e46a..ce83c255e9b 100644 --- a/modules/md/md_reg.h +++ b/modules/md/md_reg.h @@ -183,6 +183,14 @@ int md_reg_should_renew(md_reg_t *reg, const md_t *md, apr_pool_t *p); */ apr_time_t md_reg_renew_at(md_reg_t *reg, const md_t *md, apr_pool_t *p); +/* Check ACME Renewal Info, if available, for the CA recommended time + * (and optional reason URL) for certificate renewal. + * Returns 0 if not availble. + */ +apr_time_t md_reg_ari_renew_at(const char **purl, md_reg_t *reg, + const md_t *md, struct apr_table_t *env, + struct md_result_t *result, 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. @@ -233,6 +241,8 @@ typedef apr_status_t md_proto_init_preload_cb(md_proto_driver_t *driver, struct 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); +typedef apr_status_t md_proto_get_ari(md_proto_driver_t *driver, struct md_result_t *result, + apr_time_t *prenew_at, const char **purl); struct md_proto_t { const char *protocol; @@ -241,6 +251,7 @@ struct md_proto_t { md_proto_init_preload_cb *init_preload; md_proto_preload_cb *preload; md_proto_complete_md_cb *complete_md; + md_proto_get_ari *get_ari; }; /** diff --git a/modules/md/md_status.c b/modules/md/md_status.c index 5490e770108..e7d764519f7 100644 --- a/modules/md/md_status.c +++ b/modules/md/md_status.c @@ -55,8 +55,8 @@ static apr_status_t status_get_cert_json(md_json_t **pjson, const md_cert_t *cer if (issuer_name) md_json_sets(issuer_name, json, MD_KEY_ISSUER_NAME, NULL); rv = md_cert_get_issuers_uri(&issuer_uri, cert, p); - if(rv == APR_SUCCESS && issuer_uri) - md_json_sets(issuer_uri, json, MD_KEY_ISSUER_URI, NULL); + if (rv == APR_SUCCESS && issuer_uri) + md_json_sets(issuer_uri, json, MD_KEY_ISSUER_URI, NULL); 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); @@ -120,9 +120,14 @@ static apr_status_t status_get_cert_json_ex( md_json_t *certj, *jobj; md_timeperiod_t ocsp_valid; md_ocsp_cert_stat_t cert_stat; + const char *ari_cert_id; apr_status_t rv; if (APR_SUCCESS != (rv = status_get_cert_json(&certj, cert, p))) goto leave; + + if (APR_SUCCESS == md_cert_get_ari_cert_id(&ari_cert_id, cert, p)) + md_json_sets(ari_cert_id, certj, MD_KEY_ARI_CERT_ID, NULL); + if (md->stapling && ocsp) { rv = md_ocsp_get_meta(&cert_stat, &ocsp_valid, ocsp, cert, p, md); if (APR_SUCCESS == rv) { @@ -135,6 +140,7 @@ static apr_status_t status_get_cert_json_ex( md_json_setj(jobj, certj, MD_KEY_OCSP, MD_KEY_RENEWAL, NULL); } } + leave: *pjson = (APR_SUCCESS == rv)? certj : NULL; return rv; @@ -215,7 +221,7 @@ 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; + md_json_t *mdj, *certsj, *jobj = NULL; int renew; const md_pubcert_t *pubcert; const md_cert_t *cert = NULL; @@ -245,20 +251,28 @@ static apr_status_t status_get_md_json(md_json_t **pjson, const md_t *md, 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); + + renew = FALSE; + rv = job_loadj(&jobj, MD_SG_STAGING, md->name, reg, with_logs, p); + if (rv == APR_SUCCESS) + renew = TRUE; + else if (APR_STATUS_IS_ENOENT(rv)) { + rv = APR_SUCCESS; + renew = md_reg_should_renew(reg, md, p); + } + else + goto leave; + 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 (jobj) { 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); @@ -598,11 +612,19 @@ apr_time_t md_job_delay_on_errors(md_job_t *job, int err_count, const char *last 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; + /* back off duration, depending on the errors we encounter in a row. + * As apr_time_t is signed, this might wrap around*/ + int i; + delay = job->min_delay; + for (i = 0; i < err_count; ++i) { + delay <<= 1; + if ((delay <= 0) || (delay > max_delay)) { + delay = max_delay; + break; + } } + if (delay > max_delay) + delay = max_delay; } if (delay > 0) { /* jitter the delay by +/- 0-50%. diff --git a/modules/md/md_tailscale.c b/modules/md/md_tailscale.c deleted file mode 100644 index c8d2bad64c5..00000000000 --- a/modules/md/md_tailscale.c +++ /dev/null @@ -1,383 +0,0 @@ -/* 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 -#include - -#include -#include -#include -#include - -#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 deleted file mode 100644 index 67a874dc28c..00000000000 --- a/modules/md/md_tailscale.h +++ /dev/null @@ -1,25 +0,0 @@ -/* 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 index 268ca83c1b6..97db6071b1a 100644 --- a/modules/md/md_time.c +++ b/modules/md/md_time.c @@ -323,3 +323,70 @@ md_timeperiod_t md_timeperiod_common(const md_timeperiod_t *a, const md_timeperi } return c; } + +apr_time_t md_time_parse_rfc3339(const char *s) +{ + apr_time_exp_t t; + apr_time_t ts; + apr_size_t i = 0; + + memset(&t, 0, sizeof(t)); + if (!apr_isdigit(s[0]) || !apr_isdigit(s[1]) || !apr_isdigit(s[2]) || + !apr_isdigit(s[3]) || + (s[4] != '-') || + !apr_isdigit(s[5]) || !apr_isdigit(s[6]) || + (s[7] != '-') || + !apr_isdigit(s[8]) || !apr_isdigit(s[9]) || + ((s[10] != 'T') && (s[10] != 't')) || + !apr_isdigit(s[11]) || !apr_isdigit(s[12]) || + (s[13] != ':') || + !apr_isdigit(s[14]) || !apr_isdigit(s[15]) || + (s[16] != ':') || + !apr_isdigit(s[17]) || !apr_isdigit(s[18]) + ) + return 0; + t.tm_year = (s[0] - '0') * 1000; + t.tm_year += (s[1] - '0') * 100; + t.tm_year += (s[2] - '0') * 10; + t.tm_year += (s[3] - '0'); + t.tm_year -= 1900; /* the apr time 0 point */ + t.tm_mon = (s[5] - '0') * 10; + t.tm_mon += (s[6] - '0') - 1; /* -1 since January is 0 not 1. */ + t.tm_mday = (s[8] - '0') * 10; + t.tm_mday += (s[9] - '0'); + t.tm_hour = (s[11] - '0') * 10; + t.tm_hour += (s[12] - '0'); + t.tm_min = (s[14] - '0') * 10; + t.tm_min += (s[15] - '0'); + t.tm_sec = (s[17] - '0') * 10; + t.tm_sec += (s[18] - '0'); + + i = 19; + if (s[i] == '.') { + for(i += 1; apr_isdigit(s[i]); ++i) + ; /* skip for now */ + } + + if ((s[i] == 'Z') || (s[i] == 'z')) { + /* alredy GMT, done */ + t.tm_gmtoff = 0; + } + else if (((s[i] == '+') || (s[i] == '-')) && + apr_isdigit(s[i+1]) && apr_isdigit(s[i+2]) && + (s[i+3] == ':') && + apr_isdigit(s[i+4]) && apr_isdigit(s[i+5])) { + /* tm_gmtoff is in seconds */ + t.tm_gmtoff = (s[i+1] - '0') * 10 * (60 * 60); + t.tm_gmtoff += (s[i+2] - '0') * (60 * 60); + t.tm_gmtoff = (s[i+4] - '0') * 10 * 60; + t.tm_gmtoff += (s[i+5] - '0') * 60; + if (s[i] == '-') + t.tm_gmtoff *= -1; + } + else + return 0; + + if (APR_SUCCESS == apr_time_exp_gmt_get(&ts, &t)) + return ts; + return 0; +} diff --git a/modules/md/md_time.h b/modules/md/md_time.h index 92bd9d8aa97..99c610aa88d 100644 --- a/modules/md/md_time.h +++ b/modules/md/md_time.h @@ -74,4 +74,7 @@ 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); +/* parse rfc3339 timestamp, return 0 when not valid */ +apr_time_t md_time_parse_rfc3339(const char *s); + #endif /* md_util_h */ diff --git a/modules/md/md_version.h b/modules/md/md_version.h index 5b02ed369a8..8ae950a457f 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 "2.5.2" +#define MOD_MD_VERSION "2.6.1" /** * @macro @@ -35,9 +35,8 @@ * 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 0x020502 +#define MOD_MD_VERSION_NUM 0x020601 #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 8b83b4e6786..349d18750c8 100644 --- a/modules/md/mod_md.c +++ b/modules/md/mod_md.c @@ -331,7 +331,8 @@ static void merge_srv_config(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p) APR_ARRAY_PUSH(md->contacts, const char *) = md_util_schemify(p, contact, "mailto"); } - else if( md->sc->s->server_admin && strcmp(DEFAULT_ADMIN, md->sc->s->server_admin)) { + 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"); @@ -369,6 +370,9 @@ static void merge_srv_config(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p) if (md->profile_mandatory < 0) { md->profile_mandatory = md_config_geti(md->sc, MD_CONFIG_CA_PROFILE_MANDATORY); } + if (md->ari_renewals < 0) { + md->ari_renewals = md_config_geti(md->sc, MD_CONFIG_ARI_RENEWALS); + } } static apr_status_t check_coverage(md_t *md, const char *domain, server_rec *s, diff --git a/modules/md/mod_md.dsp b/modules/md/mod_md.dsp index d0365ed8531..7b247a5bc0d 100644 --- a/modules/md/mod_md.dsp +++ b/modules/md/mod_md.dsp @@ -205,10 +205,6 @@ SOURCE=./md_store_fs.c # End Source File # Begin Source File -SOURCE=./md_tailscale.c -# End Source File -# Begin Source File - SOURCE=./md_time.c # End Source File # Begin Source File diff --git a/modules/md/mod_md_config.c b/modules/md/mod_md_config.c index 413f1e8e99b..70632b22ef8 100644 --- a/modules/md/mod_md_config.c +++ b/modules/md/mod_md_config.c @@ -85,7 +85,7 @@ static md_mod_conf_t defmc = { "https://crt.sh?q=", /* default cert checker site url */ NULL, /* CA cert file to use */ apr_time_from_sec(MD_SECS_PER_DAY/2), /* default time between cert checks */ - apr_time_from_sec(5), /* minimum delay for retries */ + apr_time_from_sec(30), /* 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 */ @@ -124,6 +124,7 @@ static md_srv_conf_t defconf = { 0, /* ACME profile mandatory */ 0, /* stapling */ 1, /* staple others */ + 1, /* ACME ARI renewals */ NULL, /* dns01_cmd */ NULL, /* currently defined md */ NULL, /* assigned md, post config */ @@ -181,6 +182,7 @@ static void srv_conf_props_clear(md_srv_conf_t *sc) sc->profile_mandatory = DEF_VAL; sc->stapling = DEF_VAL; sc->staple_others = DEF_VAL; + sc->ari_renewals = DEF_VAL; sc->dns01_cmd = NULL; } @@ -204,6 +206,7 @@ static void srv_conf_props_copy(md_srv_conf_t *to, const md_srv_conf_t *from) to->profile_mandatory = from->profile_mandatory; to->stapling = from->stapling; to->staple_others = from->staple_others; + to->ari_renewals = from->ari_renewals; to->dns01_cmd = from->dns01_cmd; } @@ -229,6 +232,7 @@ static void srv_conf_props_apply(md_t *md, const md_srv_conf_t *from, apr_pool_t if (from->ca_eab_hmac) md->ca_eab_hmac = from->ca_eab_hmac; if (from->profile) md->profile = from->profile; if (from->profile_mandatory != DEF_VAL) md->profile_mandatory = from->profile_mandatory; + if (from->ari_renewals != DEF_VAL) md->ari_renewals = from->ari_renewals; if (from->stapling != DEF_VAL) md->stapling = from->stapling; if (from->dns01_cmd) md->dns01_cmd = from->dns01_cmd; } @@ -277,7 +281,7 @@ static void *md_config_merge(apr_pool_t *pool, void *basev, void *addv) nsc->profile = add->profile? add->profile : base->profile; nsc->profile_mandatory = (add->profile_mandatory != DEF_VAL)? add->profile_mandatory : base->profile_mandatory; 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->ari_renewals = (add->ari_renewals != DEF_VAL)? add->ari_renewals : base->ari_renewals; nsc->dns01_cmd = (add->dns01_cmd)? add->dns01_cmd : base->dns01_cmd; nsc->current = NULL; @@ -650,6 +654,18 @@ static const char *md_config_set_staple_others(cmd_parms *cmd, void *dc, const c return set_on_off(&config->staple_others, value, cmd->pool); } +static const char *md_config_set_ari(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; + } + return set_on_off(&config->ari_renewals, 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); @@ -689,6 +705,9 @@ static const char *md_config_set_min_delay(cmd_parms *cmd, void *dc, const char if (md_duration_parse(&delay, value, "s") != APR_SUCCESS) { return "unrecognized duration format"; } + if (delay <= 0) { + return "minimum delay must be greater than 0"; + } config->mc->min_delay = delay; return NULL; } @@ -1364,6 +1383,8 @@ const command_rec md_cmds[] = { "The name of an CA profile to order certificates for."), AP_INIT_TAKE1("MDProfileMandatory", md_config_set_profile_mandatory, NULL, RSRC_CONF, "Determines if a configured CA profile is mandatory."), + AP_INIT_TAKE1("MDRenewViaARI", md_config_set_ari, NULL, RSRC_CONF, + "Enable/Disable ACME ARI (RFC 9773) to trigger renewals."), AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL) }; @@ -1458,6 +1479,8 @@ int md_config_geti(const md_srv_conf_t *sc, md_config_var_t var) return (sc->staple_others != DEF_VAL)? sc->staple_others : defconf.staple_others; case MD_CONFIG_CA_PROFILE_MANDATORY: return (sc->profile_mandatory != DEF_VAL)? sc->profile_mandatory : defconf.profile_mandatory; + case MD_CONFIG_ARI_RENEWALS: + return (sc->ari_renewals != DEF_VAL)? sc->ari_renewals : defconf.ari_renewals; default: return 0; } diff --git a/modules/md/mod_md_config.h b/modules/md/mod_md_config.h index 48272cfdf13..a2354354cc4 100644 --- a/modules/md/mod_md_config.h +++ b/modules/md/mod_md_config.h @@ -41,6 +41,7 @@ typedef enum { MD_CONFIG_STAPLE_OTHERS, MD_CONFIG_CA_PROFILE, MD_CONFIG_CA_PROFILE_MANDATORY, + MD_CONFIG_ARI_RENEWALS, } md_config_var_t; typedef enum { @@ -110,6 +111,7 @@ typedef struct md_srv_conf_t { int stapling; /* OCSP stapling enabled */ int staple_others; /* Provide OCSP stapling for non-MD certificates */ + int ari_renewals; /* ACME ARI extension enabled */ const char *dns01_cmd; /* DNS challenge command, override global command */ diff --git a/modules/md/mod_md_drive.c b/modules/md/mod_md_drive.c index d2655b8a0c8..9dfa93a290c 100644 --- a/modules/md/mod_md_drive.c +++ b/modules/md/mod_md_drive.c @@ -105,22 +105,75 @@ static void process_drive_job(md_renew_ctx_t *dctx, md_job_t *job, apr_pool_t *p * 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) + apr_time_t renew_at, now; + const char *ari_explain_url = NULL; + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10052) "md(%s): state=%d, driving", job->mdomain, md->state); + renew_at = md_reg_renew_at(dctx->mc->reg, md, ptemp); + now = apr_time_now(); + if (md->stapling && dctx->mc->ocsp && - md_reg_has_revoked_certs(dctx->mc->reg, dctx->mc->ocsp, md, dctx->p)) { - ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10500) - "md(%s): has revoked certificates", job->mdomain); + md_reg_has_revoked_certs(dctx->mc->reg, dctx->mc->ocsp, md, ptemp)) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10500) + "md(%s): need to renew, OCSP reports revoked " + "certificate(s)", job->mdomain); + } + else if (!renew_at || renew_at <= now) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10512) + "md(%s): need to renew now", job->mdomain); } - else 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; + else { + apr_time_t ari_renew_at = 0; + char ts[APR_RFC822_DATE_LEN]; + + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10513) + "md(%s): ARI is %senabled", job->mdomain, + md->ari_renewals? "" : "not "); + if (md->ari_renewals) + ari_renew_at = md_reg_ari_renew_at(&ari_explain_url, dctx->mc->reg, md, + dctx->mc->env, result, ptemp); + if (!ari_renew_at || (ari_renew_at > renew_at)) { + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10514) + "md(%s): %sup for configured renewal in %s", + job->mdomain, + (ari_renew_at || !md->ari_renewals) ? "" : "no ARI available, ", + md_duration_print(ptemp, renew_at - now)); + goto expiry; + } + else if (ari_renew_at > now) { + long secs = (long)apr_time_sec(ari_renew_at - now); + apr_rfc822_date(ts, ari_renew_at); + if (ari_explain_url) { + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, dctx->s, APLOGNO(10515) + "md(%s): CA advises renew via ARI at %s" + ", for explanation see %s", + job->mdomain, ts, ari_explain_url); + } + else + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10516) + "md(%s): CA advises renew via ARI at %s", + job->mdomain, ts); + if (secs < MD_SECS_PER_DAY) { /* earlier than regular run */ + job->next_run = ari_renew_at; + } + goto expiry; + } + /* ARI says we should renew *now* */ + if (ari_explain_url) { + ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, dctx->s, APLOGNO(10517) + "md(%s): CA advises renew via ARI now" + ", for explanation see %s", + job->mdomain, ari_explain_url); + } + else + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10518) + "md(%s): CA advises renew via ARI now", job->mdomain); } /* The (possibly configured) event handler may veto renewals. This - * is used in cluster installtations, see #233. */ + * is used in cluster installations, 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) @@ -130,6 +183,10 @@ static void process_drive_job(md_renew_ctx_t *dctx, md_job_t *job, apr_pool_t *p } md_job_start_run(job, result, md_reg_store_get(dctx->mc->reg)); + if (ari_explain_url) + md_result_printf(result, 0, + "Renewal triggered by CA via ARI, explanation at %s", + ari_explain_url); md_reg_renew(dctx->mc->reg, md, dctx->mc->env, 0, job->error_runs, result, ptemp); md_job_end_run(job, result); @@ -159,7 +216,7 @@ static void process_drive_job(md_renew_ctx_t *dctx, md_job_t *job, apr_pool_t *p } expiry: - if (!job->finished && md_reg_should_warn(dctx->mc->reg, md, dctx->p)) { + if (!job->finished && md_reg_should_warn(dctx->mc->reg, md, ptemp)) { 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)); diff --git a/modules/md/mod_md_status.c b/modules/md/mod_md_status.c index 72cff4180fc..c83e73d17aa 100644 --- a/modules/md/mod_md_status.c +++ b/modules/md/mod_md_status.c @@ -354,10 +354,8 @@ static void val_url_print(status_ctx *ctx, const status_info *info, { const char *s; - if (proto && !strcmp(proto, "tailscale")) { - s = "tailscale"; - } - else if (url) { + (void)proto; + if (url) { s = md_get_ca_name_from_url(ctx->p, url); } else { diff --git a/test/modules/md/md_conf.py b/test/modules/md/md_conf.py index 19d4977f004..54b10abc659 100755 --- a/test/modules/md/md_conf.py +++ b/test/modules/md/md_conf.py @@ -15,6 +15,8 @@ class MDConf(HttpdConf): self.add_admin(admin) self.add([ "MDRetryDelay 1s", # speed up testing a little + "MDRenewViaARI off", # not on, logs unwanted errors when test + # acme server is not responding ]) if local_ca: self.add([ diff --git a/test/modules/md/test_702_auto.py b/test/modules/md/test_702_auto.py index df24290f576..7246b62255c 100644 --- a/test/modules/md/test_702_auto.py +++ b/test/modules/md/test_702_auto.py @@ -299,7 +299,7 @@ class TestAutov2: # Force cert renewal due to critical remaining valid duration # Assert that new cert activation is delayed - def test_md_702_009(self, env): + def test_md_702_009(self, env, acme): domain = self.test_domain domains = [domain] # @@ -329,6 +329,7 @@ class TestAutov2: assert env.await_completion([domain], must_renew=True) stat = env.get_certificate_status(domain) assert creds.certificate.serial_number != int(stat['rsa']['serial'], 16) + env.httpd_error_log.clear_log() # test case: drive with an unsupported challenge due to port availability def test_md_702_010(self, env): diff --git a/test/modules/md/test_710_profiles.py b/test/modules/md/test_710_profiles.py index 2fbcf267ae3..a7faad64f0b 100644 --- a/test/modules/md/test_710_profiles.py +++ b/test/modules/md/test_710_profiles.py @@ -122,7 +122,7 @@ class TestProfiles: assert stat["profile"] == "XXX", f'{stat}' assert len(stat['cert']) == 0, f'{stat}' assert stat['renewal']['errors'] > 0, f'{stat}' - assert stat['renewal']['last']['activity'] == 'Creating new order', f'{stat}' + assert stat['renewal']['last']['activity'] == 'Creating new order, key-spec=default, profile=XXX, replacing-cert=none', f'{stat}' MDConf(env).install() assert env.apache_restart() == 0, f'{env.apachectl_stderr}' env.httpd_error_log.ignore_recent(matches=[ diff --git a/test/modules/md/test_730_static.py b/test/modules/md/test_730_static.py index 3c4d26e88c9..bc3f20ce6b3 100644 --- a/test/modules/md/test_730_static.py +++ b/test/modules/md/test_730_static.py @@ -58,6 +58,11 @@ class TestStatic: assert 'cert' in stat assert stat['renew'] is True assert 'renewal' not in stat + env.httpd_error_log.ignore_recent( + matches = [ + r'.*cert has no authority key id extension.*' + ] + ) def test_md_730_002(self, env): # MD with static cert files, force driving @@ -94,6 +99,11 @@ class TestStatic: assert 'cert' in stat['renewal'] assert 'secp384r1' in stat['renewal']['cert'] assert 'rsa' in stat['renewal']['cert'] + env.httpd_error_log.ignore_recent( + matches = [ + r'.*cert has no authority key id extension.*' + ] + ) def test_md_730_003(self, env): # just configuring one file will not work diff --git a/test/modules/md/test_780_tailscale.py b/test/modules/md/test_780_tailscale.py deleted file mode 100644 index bb218f90c18..00000000000 --- a/test/modules/md/test_780_tailscale.py +++ /dev/null @@ -1,198 +0,0 @@ -import os -import re -import socket -import sys -from threading import Thread - -import pytest - -from .md_conf import MDConf - - -class TailscaleFaker: - - def __init__(self, env, path): - self.env = env - self._uds_path = path - self._done = False - - def start(self): - def process(self): - self._socket.listen(1) - self._process() - - try: - os.unlink(self._uds_path) - except OSError: - if os.path.exists(self._uds_path): - raise - self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self._socket.bind(self._uds_path) - self._thread = Thread(target=process, daemon=True, args=[self]) - self._thread.start() - - def stop(self): - self._done = True - self._socket.close() - - def send_error(self, c, status, reason): - c.sendall(f"""HTTP/1.1 {status} {reason}\r -Server: TailscaleFaker\r -Content-Length: 0\r -Connection: close\r -\r -""".encode()) - - def send_data(self, c, ctype: str, data: bytes): - c.sendall(f"""HTTP/1.1 200 OK\r -Server: TailscaleFaker\r -Content-Type: {ctype}\r -Content-Length: {len(data)}\r -Connection: close\r -\r -""".encode() + data) - - def _process(self): - # a http server written on a sunny afternooon - while self._done is False: - try: - c, client_address = self._socket.accept() - try: - data = c.recv(1024) - lines = data.decode().splitlines() - m = re.match(r'^(?P\w+)\s+(?P\S+)\s+HTTP/1.1', lines[0]) - if m is None: - self.send_error(c, 400, "Bad Request") - continue - uri = m.group('uri') - m = re.match(r'/localapi/v0/cert/(?P\S+)\?type=(?P\w+)', uri) - if m is None: - self.send_error(c, 404, "Not Found") - continue - domain = m.group('domain') - cred_type = m.group('type') - creds = self.env.get_credentials_for_name(domain) - sys.stderr.write(f"lookup domain={domain}, type={cred_type} -> {creds}\n") - if creds is None or len(creds) == 0: - self.send_error(c, 404, "Not Found") - continue - if cred_type == 'crt': - self.send_data(c, "text/plain", creds[0].cert_pem) - pass - elif cred_type == 'key': - self.send_data(c, "text/plain", creds[0].pkey_pem) - else: - self.send_error(c, 404, "Not Found") - continue - finally: - c.close() - - except ConnectionAbortedError: - self._done = True - - -class TestTailscale: - - @pytest.fixture(autouse=True, scope='class') - def _class_scope(self, env, acme): - UDS_PATH = f"{env.gen_dir}/tailscale.sock" - TestTailscale.UDS_PATH = UDS_PATH - faker = TailscaleFaker(env=env, path=UDS_PATH) - faker.start() - env.APACHE_CONF_SRC = "data/test_auto" - acme.start(config='default') - env.clear_store() - MDConf(env).install() - assert env.apache_restart() == 0, f'{env.apachectl_stderr}' - yield - faker.stop() - - @pytest.fixture(autouse=True, scope='function') - def _method_scope(self, env, request): - env.clear_store() - self.test_domain = env.get_request_domain(request) - - def _write_res_file(self, doc_root, name, content): - if not os.path.exists(doc_root): - os.makedirs(doc_root) - open(os.path.join(doc_root, name), "w").write(content) - - # create a MD using `tailscale` as protocol, wrong path - def test_md_780_001(self, env): - domain = env.tailscale_domain - # generate config with one MD - domains = [domain] - socket_path = '/xxx' - conf = MDConf(env, admin="admin@" + domain) - conf.start_md(domains) - conf.add([ - "MDCertificateProtocol tailscale", - f"MDCertificateAuthority file://{socket_path}", - ]) - conf.end_md() - conf.add_vhost(domains) - conf.install() - # restart and watch it fail due to wrong tailscale unix socket path - assert env.apache_restart() == 0, f'{env.apachectl_stderr}' - md = env.await_error(domain) - assert md - assert md['renewal']['errors'] > 0 - assert md['renewal']['last']['status-description'] == 'No such file or directory' - assert md['renewal']['last']['detail'] == \ - f"tailscale socket not available, may not be up: {socket_path}" - # - env.httpd_error_log.ignore_recent( - lognos = [ - "AH10056" # retrieving certificate from tailscale - ] - ) - - # create a MD using `tailscale` as protocol, path to faker, should succeed - def test_md_780_002(self, env): - domain = env.tailscale_domain - # generate config with one MD - domains = [domain] - socket_path = '/xxx' - conf = MDConf(env, admin="admin@" + domain) - conf.start_md(domains) - conf.add([ - "MDCertificateProtocol tailscale", - f"MDCertificateAuthority file://{self.UDS_PATH}", - ]) - conf.end_md() - conf.add_vhost(domains) - conf.install() - # restart and watch it fail due to wrong tailscale unix socket path - assert env.apache_restart() == 0, f'{env.apachectl_stderr}' - assert env.await_completion(domains) - assert env.apache_restart() == 0, f'{env.apachectl_stderr}' - env.check_md_complete(domain) - - # create a MD using `tailscale` as protocol, but domain name not assigned by tailscale - def test_md_780_003(self, env): - domain = "test.not-correct.ts.net" - # generate config with one MD - domains = [domain] - socket_path = '/xxx' - conf = MDConf(env, admin="admin@" + domain) - conf.start_md(domains) - conf.add([ - "MDCertificateProtocol tailscale", - f"MDCertificateAuthority file://{self.UDS_PATH}", - ]) - conf.end_md() - conf.add_vhost(domains) - conf.install() - # restart and watch it fail due to wrong tailscale unix socket path - assert env.apache_restart() == 0, f'{env.apachectl_stderr}' - md = env.await_error(domain) - assert md - assert md['renewal']['errors'] > 0 - assert md['renewal']['last']['status-description'] == 'No such file or directory' - assert md['renewal']['last']['detail'] == "retrieving certificate from tailscale" - # - env.httpd_error_log.ignore_recent( - lognos = [ - "AH10056" # retrieving certificate from tailscale - ] - ) diff --git a/test/modules/md/test_920_status.py b/test/modules/md/test_920_status.py index d9babb172b3..a74ac3c1df6 100644 --- a/test/modules/md/test_920_status.py +++ b/test/modules/md/test_920_status.py @@ -215,6 +215,11 @@ Protocols h2 http/1.1 acme-tls/1 print(status) assert status['state'] == env.MD_S_COMPLETE assert status['renew-mode'] == 0 # manual + env.httpd_error_log.ignore_recent( + matches = [ + r'.*cert has no authority key id extension.*' + ] + ) # MD with 2 certificates def test_md_920_020(self, env): -- 2.47.2