]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
*) mod_md: update to version 2.6.1
authorStefan Eissing <icing@apache.org>
Fri, 15 Aug 2025 11:23:29 +0000 (11:23 +0000)
committerStefan Eissing <icing@apache.org>
Fri, 15 Aug 2025 11:23:29 +0000 (11:23 +0000)
     - 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

36 files changed:
changes-entries/md_v2.6.1.txt [new file with mode: 0644]
docs/manual/mod/mod_md.xml
modules/md/config2.m4
modules/md/md.h
modules/md/md_acme.c
modules/md/md_acme.h
modules/md/md_acme_authz.c
modules/md/md_acme_drive.c
modules/md/md_acme_order.c
modules/md/md_acme_order.h
modules/md/md_acmev2_drive.c
modules/md/md_core.c
modules/md/md_crypt.c
modules/md/md_crypt.h
modules/md/md_http.c
modules/md/md_json.c
modules/md/md_reg.c
modules/md/md_reg.h
modules/md/md_status.c
modules/md/md_tailscale.c [deleted file]
modules/md/md_tailscale.h [deleted file]
modules/md/md_time.c
modules/md/md_time.h
modules/md/md_version.h
modules/md/mod_md.c
modules/md/mod_md.dsp
modules/md/mod_md_config.c
modules/md/mod_md_config.h
modules/md/mod_md_drive.c
modules/md/mod_md_status.c
test/modules/md/md_conf.py
test/modules/md/test_702_auto.py
test/modules/md/test_710_profiles.py
test/modules/md/test_730_static.py
test/modules/md/test_780_tailscale.py [deleted file]
test/modules/md/test_920_status.py

diff --git a/changes-entries/md_v2.6.1.txt b/changes-entries/md_v2.6.1.txt
new file mode 100644 (file)
index 0000000..f3d1c30
--- /dev/null
@@ -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.
index 09bae08a3bbcce601a0fb9e4fabf6b626efa0fd6..c383294c0da1afd4ce8e490c1842ee051dd97365 100644 (file)
@@ -1393,7 +1393,7 @@ MDMessageCmd /etc/apache/md-message
         <name>MDRetryDelay</name>
         <description>Time length for first retry, doubled on every consecutive error.</description>
         <syntax>MDRetryDelay <var>duration</var></syntax>
-        <default>MDRetryDelay 5s</default>
+        <default>MDRetryDelay 30s</default>
         <contextlist>
             <context>server config</context>
         </contextlist>
@@ -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.
             </p>
+            <p>
+                In mod_md v2.6.1, the default delay has been increased from 5
+                seconds to 30.
+            </p>
         </usage>
     </directivesynopsis>
 
@@ -1594,4 +1598,26 @@ MDMessageCmd /etc/apache/md-message
             </p>
         </usage>
     </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDRenewViaARI</name>
+        <description>usage of the ACME ARI extension (rfc9773).</description>
+        <syntax>MDRenewViaARI on|off</syntax>
+        <default>MDRenewViaARI on</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>
+                En-/Disable certificate renewals triggered via the ACME ARI
+                extension (rfc9773). These renewals happen *in addition* to
+                the mechanism controlled by <directive>MDRenewWindow</directive>.
+           </p><p>
+                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.
+            </p>
+        </usage>
+    </directivesynopsis>
 </modulesynopsis>
index e416736a8c1a02315060e17659d5ca95a4247d45..b20ab3b45e1bae39c552fc36eb920de5c7c96c17 100644 (file)
@@ -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
index 3f298eaa6f33e33ebee6d2d337f01966522bc7a3..fb1a270ac8fc6c3ace928d094260faa550da60cd 100644 (file)
@@ -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"
index f8624513ba8e10655157cf8a5635687935c2d325..33a7afa310a673bd5e172267cba46112172ae3c9 100644 (file)
@@ -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 <https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html#rfc.section.6.3>
          * and <https://mailarchive.ietf.org/arch/msg/acme/sotffSQ0OWV-qQJodLwWYWcEVKI>
          * and <https://community.letsencrypt.org/t/acme-v2-scheduled-deprecation-of-unauthenticated-resource-gets/74380>
@@ -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"
index 9931f92493ff9b80f8ec4ce2d34251d61d86c36d..c2b98b412d0655671fa2893dc03f35a0146ad081 100644 (file)
@@ -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);
index fc46274fffd5b53c437f12c93a9a8e53ff26a247..c77dcdbaeb3334a3f244798b69d76841b1271d70 100644 (file)
@@ -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); 
index f5cd08c5214f53fa69c6dd5436e094417eb87075..94bcc8aab47b9746d8addea7dea6c82cf5d64ad1 100644 (file)
@@ -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)
index 0a0ad7ff0aee943b80019f271e246d7834b0f9f3..22d84a2594acc0a6877f3891e341c11105bc6aad 100644 (file)
@@ -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);
index 01d73d41b9f4921908b8467bd58bb5d73f048e64..2d08cd9861a417924c55249a2ca70d13b13ed4ca 100644 (file)
@@ -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);
index e5821e560aef83a5046811c742d2f80eed6fe35e..de58c7247ca5dd500978adf123ca5b93d12deef2 100644 (file)
@@ -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) {
index 70f20c4bebe3262b8f9b5d5963448aa32de5debf..95b289d2d488d5c7d4387ef2d179dcd0a1c23355 100644 (file)
@@ -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;
index e56a2c0c9b73b2b68567d2e03d51e6a1a3adc518..c5a6dd6f9ebea969d5797e19f9c86b41f6da8eb7 100644 (file)
@@ -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
+}
+
index e6b3ac2e783e4ecc2af7f693d77adcb4d5a57cfd..6880cd2c095d2f36d11f45a09d487519504ad985 100644 (file)
@@ -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 */
index 0d21e7b14c6d3cddd03b793370403baa9bb9fbc1..283f4be13528bf20c900114237dea5291528fa84 100644 (file)
@@ -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;
 }
index e0f977ea564b5dbf021e9b610d98cd3cec0afc35..bd0e1c5a3b707e84de2d070634962a417381cc2c 100644 (file)
@@ -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:
index d0a41de177d0882fb16b97249c5a765b20b5a018..5da2f6dd4bcd14253ae3122cfb970bce34ec23dd 100644 (file)
@@ -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(&reg->renew_window, p, MD_TIME_LIFE_NORM, MD_TIME_RENEW_WINDOW_DEF); 
     md_timeslice_create(&reg->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) {
index 191b026e46af74fe9a048ee6592cc40bb1e168c6..ce83c255e9bf2d92c69797dbbf2bf920364fd1f5 100644 (file)
@@ -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;
 };
 
 /**
index 5490e770108b22757657b9a20ef661718f9bae8c..e7d764519f7e773708543fc7cbb7eff0457beee1 100644 (file)
@@ -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 (file)
index c8d2bad..0000000
+++ /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 <assert.h>
-#include <stdlib.h>
-
-#include <apr_lib.h>
-#include <apr_strings.h>
-#include <apr_hash.h>
-#include <apr_uri.h>
-
-#include "md.h"
-#include "md_crypt.h"
-#include "md_json.h"
-#include "md_http.h"
-#include "md_log.h"
-#include "md_result.h"
-#include "md_reg.h"
-#include "md_store.h"
-#include "md_util.h"
-
-#include "md_tailscale.h"
-
-typedef struct {
-    apr_pool_t *pool;
-    md_proto_driver_t *driver;
-    const char *unix_socket_path;
-    md_t *md;
-    apr_array_header_t *chain;
-    md_pkey_t *pkey;
-} ts_ctx_t;
-
-static apr_status_t ts_init(md_proto_driver_t *d, md_result_t *result)
-{
-    ts_ctx_t *ts_ctx;
-    apr_uri_t uri;
-    const char *ca_url;
-    apr_status_t rv = APR_SUCCESS;
-
-    md_result_set(result, APR_SUCCESS, NULL);
-    ts_ctx = apr_pcalloc(d->p, sizeof(*ts_ctx));
-    ts_ctx->pool = d->p;
-    ts_ctx->driver = d;
-    ts_ctx->chain = apr_array_make(d->p, 5, sizeof(md_cert_t *));
-
-    ca_url = (d->md->ca_urls && !apr_is_empty_array(d->md->ca_urls))?
-                APR_ARRAY_IDX(d->md->ca_urls, 0, const char*) : NULL;
-    if (!ca_url) {
-        ca_url = MD_TAILSCALE_DEF_URL;
-    }
-    rv = apr_uri_parse(d->p, ca_url, &uri);
-    if (APR_SUCCESS != rv) {
-        md_result_printf(result, rv, "error parsing CA URL `%s`", ca_url);
-        goto leave;
-    }
-    if (uri.scheme && uri.scheme[0] && strcmp("file", uri.scheme)) {
-        rv = APR_ENOTIMPL;
-        md_result_printf(result, rv, "non `file` URLs not supported, CA URL is `%s`",
-                         ca_url);
-        goto leave;
-    }
-    if (uri.hostname && uri.hostname[0] && strcmp("localhost", uri.hostname)) {
-        rv = APR_ENOTIMPL;
-        md_result_printf(result, rv, "non `localhost` URLs not supported, CA URL is `%s`",
-                         ca_url);
-        goto leave;
-    }
-    ts_ctx->unix_socket_path = uri.path;
-    d->baton = ts_ctx;
-
-leave:
-    return rv;
-}
-
-static apr_status_t ts_preload_init(md_proto_driver_t *d, md_result_t *result)
-{
-    return ts_init(d, result);
-}
-
-static apr_status_t ts_preload(md_proto_driver_t *d,
-                               md_store_group_t load_group, md_result_t *result)
-{
-    apr_status_t rv;
-    md_t *md;
-    md_credentials_t *creds;
-    md_pkey_spec_t *pkspec;
-    apr_array_header_t *all_creds;
-    const char *name;
-    int i;
-
-    name = d->md->name;
-    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: preload start", name);
-    /* Load data from MD_SG_STAGING and save it into "load_group".
-     */
-    if (APR_SUCCESS != (rv = md_load(d->store, MD_SG_STAGING, name, &md, d->p))) {
-        md_result_set(result, rv, "loading staged md.json");
-        goto leave;
-    }
-
-     /* tailscale generates one cert+key with key specification being whatever
-      * it chooses. Use the NULL spec here.
-      */
-    all_creds = apr_array_make(d->p, 5, sizeof(md_credentials_t*));
-    pkspec = NULL;
-    if (APR_SUCCESS != (rv = md_creds_load(d->store, MD_SG_STAGING, name, pkspec, &creds, d->p))) {
-        md_result_printf(result, rv, "loading staged credentials");
-        goto leave;
-    }
-    if (!creds->chain) {
-        rv = APR_ENOENT;
-        md_result_printf(result, rv, "no certificate in staged credentials");
-        goto leave;
-    }
-    if (APR_SUCCESS != (rv = md_check_cert_and_pkey(creds->chain, creds->pkey))) {
-        md_result_printf(result, rv, "certificate and private key do not match in staged credentials");
-        goto leave;
-    }
-    APR_ARRAY_PUSH(all_creds, md_credentials_t*) = creds;
-
-    md_result_activity_setn(result, "purging store tmp space");
-    rv = md_store_purge(d->store, d->p, load_group, name);
-    if (APR_SUCCESS != rv) {
-        md_result_set(result, rv, NULL);
-        goto leave;
-    }
-
-    md_result_activity_setn(result, "saving staged md/privkey/pubcert");
-    if (APR_SUCCESS != (rv = md_save(d->store, d->p, load_group, md, 1))) {
-        md_result_set(result, rv, "writing md.json");
-        goto leave;
-    }
-
-    for (i = 0; i < all_creds->nelts; ++i) {
-        creds = APR_ARRAY_IDX(all_creds, i, md_credentials_t*);
-        if (APR_SUCCESS != (rv = md_creds_save(d->store, d->p, load_group, name, creds, 1))) {
-            md_result_printf(result, rv, "writing credentials #%d", i);
-            goto leave;
-        }
-    }
-
-    md_result_set(result, APR_SUCCESS, "saved staged data successfully");
-
-leave:
-    md_result_log(result, MD_LOG_DEBUG);
-    return rv;
-}
-
-static apr_status_t rv_of_response(const md_http_response_t *res)
-{
-    switch (res->status) {
-        case 200:
-            return APR_SUCCESS;
-        case 400:
-            return APR_EINVAL;
-        case 401: /* sectigo returns this instead of 403 */
-        case 403:
-            return APR_EACCES;
-        case 404:
-            return APR_ENOENT;
-        default:
-            return APR_EGENERAL;
-    }
-    return APR_SUCCESS;
-}
-
-static apr_status_t on_get_cert(const md_http_response_t *res, void *baton)
-{
-    ts_ctx_t *ts_ctx = baton;
-    apr_status_t rv;
-
-    rv = rv_of_response(res);
-    if (APR_SUCCESS != rv) goto leave;
-    apr_array_clear(ts_ctx->chain);
-    rv = md_cert_chain_read_http(ts_ctx->chain, ts_ctx->pool, res);
-    if (APR_SUCCESS != rv) goto leave;
-
-leave:
-    return rv;
-}
-
-static apr_status_t on_get_key(const md_http_response_t *res, void *baton)
-{
-    ts_ctx_t *ts_ctx = baton;
-    apr_status_t rv;
-
-    rv = rv_of_response(res);
-    if (APR_SUCCESS != rv) goto leave;
-    rv = md_pkey_read_http(&ts_ctx->pkey, ts_ctx->pool, res);
-    if (APR_SUCCESS != rv) goto leave;
-
-leave:
-    return rv;
-}
-
-static apr_status_t ts_renew(md_proto_driver_t *d, md_result_t *result)
-{
-    const char *name, *domain, *url;
-    apr_status_t rv = APR_ENOENT;
-    ts_ctx_t *ts_ctx = d->baton;
-    md_http_t *http;
-    const md_pubcert_t *pubcert;
-    md_cert_t *old_cert, *new_cert;
-    int reset_staging = d->reset;
-
-    /* "renewing" the certificate from tailscale. Since tailscale has its
-     * own ideas on when to do this, we can only inspect the certificate
-     * it gives us and see if it is different from the current one we have.
-     * (if we have any. first time, lacking a cert, any it gives us is
-     *  considered as 'renewed'.)
-     */
-    name = d->md->name;
-    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: renewing cert", name);
-
-    /* When not explicitly told to reset, we check the existing data. If
-     * it is incomplete or old, we trigger the reset for a clean start. */
-    if (!reset_staging) {
-        md_result_activity_setn(result, "Checking staging area");
-        rv = md_load(d->store, MD_SG_STAGING, d->md->name, &ts_ctx->md, d->p);
-        if (APR_SUCCESS == rv) {
-            /* So, we have a copy in staging, but is it a recent or an old one? */
-            if (md_is_newer(d->store, MD_SG_DOMAINS, MD_SG_STAGING, d->md->name, d->p)) {
-                reset_staging = 1;
-            }
-        }
-        else if (APR_STATUS_IS_ENOENT(rv)) {
-            reset_staging = 1;
-            rv = APR_SUCCESS;
-        }
-    }
-
-    if (reset_staging) {
-        md_result_activity_setn(result, "Resetting staging area");
-        /* reset the staging area for this domain */
-        rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name);
-        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p,
-                      "%s: reset staging area", d->md->name);
-        if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) {
-            md_result_printf(result, rv, "resetting staging area");
-            goto leave;
-        }
-        rv = APR_SUCCESS;
-        ts_ctx->md = NULL;
-    }
-
-    if (!ts_ctx->md || !md_array_str_eq(ts_ctx->md->ca_urls, d->md->ca_urls, 1)) {
-        md_result_activity_printf(result, "Resetting staging for %s", d->md->name);
-        /* re-initialize staging */
-        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: setup staging", d->md->name);
-        md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name);
-        ts_ctx->md = md_copy(d->p, d->md);
-        rv = md_save(d->store, d->p, MD_SG_STAGING, ts_ctx->md, 0);
-        if (APR_SUCCESS != rv) {
-            md_result_printf(result, rv, "Saving MD information in staging area.");
-            md_result_log(result, MD_LOG_ERR);
-            goto leave;
-        }
-    }
-
-    if (!ts_ctx->unix_socket_path) {
-        rv = APR_ENOTIMPL;
-        md_result_set(result, rv, "only unix sockets are supported for tailscale connections");
-        goto leave;
-    }
-
-    rv = md_util_is_unix_socket(ts_ctx->unix_socket_path, d->p);
-    if (APR_SUCCESS != rv) {
-        md_result_printf(result, rv, "tailscale socket not available, may not be up: %s",
-                         ts_ctx->unix_socket_path);
-        goto leave;
-    }
-
-    rv = md_http_create(&http, d->p,
-                        apr_psprintf(d->p, "Apache mod_md/%s", MOD_MD_VERSION),
-                        NULL);
-    if (APR_SUCCESS != rv) {
-        md_result_set(result, rv, "creating http context");
-        goto leave;
-    }
-    md_http_set_unix_socket_path(http, ts_ctx->unix_socket_path);
-
-    domain = (d->md->domains->nelts > 0)?
-              APR_ARRAY_IDX(d->md->domains, 0, const char*) : NULL;
-    if (!domain) {
-        rv = APR_EINVAL;
-        md_result_set(result, rv, "no domain names available");
-    }
-
-    url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=crt",
-                       domain);
-    rv = md_http_GET_perform(http, url, NULL, on_get_cert, ts_ctx);
-    if (APR_SUCCESS != rv) {
-        md_result_set(result, rv, "retrieving certificate from tailscale");
-        goto leave;
-    }
-    if (ts_ctx->chain->nelts <= 0) {
-        rv = APR_ENOENT;
-        md_result_set(result, rv, "tailscale returned no certificates");
-        goto leave;
-    }
-
-    /* Got the key and the chain, is it new? */
-    rv = md_reg_get_pubcert(&pubcert, d->reg,d->md, 0, d->p);
-    if (APR_SUCCESS == rv) {
-        old_cert = APR_ARRAY_IDX(pubcert->certs, 0, md_cert_t*);
-        new_cert = APR_ARRAY_IDX(ts_ctx->chain, 0, md_cert_t*);
-        if (md_certs_are_equal(old_cert, new_cert)) {
-            /* tailscale has not renewed the certificate, yet */
-            rv = APR_ENOENT;
-            md_result_set(result, rv, "tailscale has not renewed the certificate yet");
-            /* let's check this daily */
-            md_result_delay_set(result, apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY));
-            goto leave;
-        }
-    }
-
-    /* We have a new certificate (or had none before).
-     * Get the key and store both in STAGING.
-     */
-    url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=key",
-                       domain);
-    rv = md_http_GET_perform(http, url, NULL, on_get_key, ts_ctx);
-    if (APR_SUCCESS != rv) {
-        md_result_set(result, rv, "retrieving key from tailscale");
-        goto leave;
-    }
-
-    rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, name, NULL, ts_ctx->pkey, 1);
-    if (APR_SUCCESS != rv) {
-        md_result_set(result, rv, "saving private key");
-        goto leave;
-    }
-
-    rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, name,
-                         NULL, ts_ctx->chain, 1);
-    if (APR_SUCCESS != rv) {
-        md_result_printf(result, rv, "saving new certificate chain.");
-        goto leave;
-    }
-
-    md_result_set(result, APR_SUCCESS,
-        "A new tailscale certificate has been retrieved successfully and can "
-        "be used. A graceful server restart is recommended.");
-
-leave:
-    md_result_log(result, MD_LOG_DEBUG);
-    return rv;
-}
-
-static apr_status_t ts_complete_md(md_t *md, apr_pool_t *p)
-{
-    (void)p;
-    if (!md->ca_urls) {
-        md->ca_urls = apr_array_make(p, 3, sizeof(const char *));
-        APR_ARRAY_PUSH(md->ca_urls, const char*) = MD_TAILSCALE_DEF_URL;
-    }
-    return APR_SUCCESS;
-}
-
-
-static md_proto_t TAILSCALE_PROTO = {
-    MD_PROTO_TAILSCALE, ts_init, ts_renew,
-    ts_preload_init, ts_preload, ts_complete_md,
-};
-
-apr_status_t md_tailscale_protos_add(apr_hash_t *protos, apr_pool_t *p)
-{
-    (void)p;
-    apr_hash_set(protos, MD_PROTO_TAILSCALE, sizeof(MD_PROTO_TAILSCALE)-1, &TAILSCALE_PROTO);
-    return APR_SUCCESS;
-}
diff --git a/modules/md/md_tailscale.h b/modules/md/md_tailscale.h
deleted file mode 100644 (file)
index 67a874d..0000000
+++ /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 */
-
index 268ca83c1b69d9597b29cdc2a9118c5e9671a1a8..97db6071b1aa42e05893f288545d540e06ce9a48 100644 (file)
@@ -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;
+}
index 92bd9d8aa97064c0317c444ec12802e8be2e376a..99c610aa88db86abffd542af7befc8332be24afa 100644 (file)
@@ -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 */
index 5b02ed369a8dd9c4f142bba7f794c3338a03bf98..8ae950a457f9b55c9645e07ed3c3bc51e2aa69ab 100644 (file)
@@ -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 */
index 8b83b4e67869096043a3113a7d446019be3af5e3..349d18750c8b813a4a17cc0014d198ccbcfe310b 100644 (file)
@@ -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,
index d0365ed85312af235c96ad8a50c4c77f1cabf4af..7b247a5bc0d4d9e06438a157adf4eeaef1402454 100644 (file)
@@ -205,10 +205,6 @@ SOURCE=./md_store_fs.c
 # End Source File\r
 # Begin Source File\r
 \r
-SOURCE=./md_tailscale.c\r
-# End Source File\r
-# Begin Source File\r
-\r
 SOURCE=./md_time.c\r
 # End Source File\r
 # Begin Source File\r
index 413f1e8e99bfa74902d16bedcc595af23a085843..70632b22ef852a1d38cafd5b069a1404a16d2c25 100644 (file)
@@ -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;
     }
index 48272cfdf13239a106262fe3c2100097b72db389..a2354354cc440da4752c8431edd6b760b1d002a4 100644 (file)
@@ -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 */
 
index d2655b8a0c8eee64c23bd8b44803311b86eb987f..9dfa93a290caad8af9c78018c5c6311769c8b756 100644 (file)
@@ -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));
index 72cff4180fce48abfefab36bed691bb50858698b..c83e73d17aa95911382c57bdfcdf42715a55ba40 100644 (file)
@@ -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 {
index 19d4977f004582e4f8130506e2d1573841e077b3..54b10abc65905f03c68e32f788b00a1a44cd2294 100755 (executable)
@@ -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([
index df24290f5767dd558066c89409713f3d163fbc14..7246b62255cee66b32c17c5d9d14f6d4dfe66018 100644 (file)
@@ -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):
index 2fbcf267ae3e1d41557dfbbc5ad448567f548926..a7faad64f0bed9c7a78a4364b3636f925aeef822 100644 (file)
@@ -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=[
index 3c4d26e88c90ae159cb1745a97ef54707fdd68dc..bc3f20ce6b30de62c418349094b84c8031144a80 100644 (file)
@@ -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 (file)
index bb218f9..0000000
+++ /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<method>\w+)\s+(?P<uri>\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<domain>\S+)\?type=(?P<type>\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
-            ]
-        )
index d9babb172b36c79a2884da33b4efaadfb78e7c4e..a74ac3c1df67f25ae0c165c75d10f3f8556d72d2 100644 (file)
@@ -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):