]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
Merge of 1893969,1894610,1894718,1895285,1895287 from trunk:
authorStefan Eissing <icing@apache.org>
Wed, 24 Nov 2021 11:07:53 +0000 (11:07 +0000)
committerStefan Eissing <icing@apache.org>
Wed, 24 Nov 2021 11:07:53 +0000 (11:07 +0000)
  *) mod_md: Fix memory leak in case of failures to load the private key.
     PR 65620 [ Filipe Casal <filipe.casal@trailofbits.com> ]
  *) mod_md: adding v2.4.8 with the following changes
    - Added support for ACME External Account Binding (EAB).
      Use the new directive `MDExternalAccountBinding` to provide the
      server with the value for key identifier and hmac as provided by
      your CA.
      While working on some servers, EAB handling is not uniform
      across CAs. First tests with a Sectigo Certificate Manager in
      demo mode are successful. But ZeroSSL, for example, seems to
      regard EAB values as a one-time-use-only thing, which makes them
      fail if you create a seconde account or retry the creation of the
      first account with the same EAB.
    - The directive 'MDCertificateAuthority' now checks if its parameter
      is a http/https url or one of a set of known names. Those are
      'LetsEncrypt', 'LetsEncrypt-Test', 'Buypass' and 'Buypass-Test'
      for now and they are not case-sensitive.
      The default of LetsEncrypt is unchanged.
    - `MDContactEmail` can now be specified inside a `<MDomain dnsname>`
      section.
    - Treating 401 HTTP status codes for orders like 403, since some ACME
      servers seem to prefer that for accessing oders from other accounts.
    - When retrieving certificate chains, try to read the repsonse even
      if the HTTP Content-Type is unrecognized.
    - Fixed a bug that reset the error counter of a certificate renewal
      and prevented the increasing delays in further attempts.
    - Fixed the renewal process giving up every time on an already existing
      order with some invalid domains. Now, if such are seen in a previous
      order, a new order is created for a clean start over again.
      See <https://github.com/icing/mod_md/issues/268>
    - Fixed a mixup in md-status handler when static certificate files
      and renewal was configured at the same time.
  *) mod_md: values for External Account Binding (EAB) can
     now also be configured to be read from a separate JSON
     file. This allows to keep server configuration permissions
     world readable without exposing secrets.

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x@1895289 13f79535-47bb-0310-9956-ffa450edef68

27 files changed:
changes-entries/md_2.4.8.txt [new file with mode: 0644]
changes-entries/md_2.4.9.txt [new file with mode: 0644]
changes-entries/pr65620.txt [new file with mode: 0644]
docs/manual/mod/mod_md.xml
modules/md/md.h
modules/md/md_acme.c
modules/md/md_acme.h
modules/md/md_acme_acct.c
modules/md/md_acme_acct.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_jws.c
modules/md/md_jws.h
modules/md/md_reg.c
modules/md/md_status.c
modules/md/md_store_fs.c
modules/md/md_version.h
modules/md/mod_md.c
modules/md/mod_md_config.c
modules/md/mod_md_config.h
modules/md/mod_md_status.c

diff --git a/changes-entries/md_2.4.8.txt b/changes-entries/md_2.4.8.txt
new file mode 100644 (file)
index 0000000..e9d099f
--- /dev/null
@@ -0,0 +1,31 @@
+  *) mod_md: adding v2.4.8 with the following changes
+    - Added support for ACME External Account Binding (EAB).
+      Use the new directive `MDExternalAccountBinding` to provide the
+      server with the value for key identifier and hmac as provided by
+      your CA.
+      While working on some servers, EAB handling is not uniform
+      across CAs. First tests with a Sectigo Certificate Manager in
+      demo mode are successful. But ZeroSSL, for example, seems to
+      regard EAB values as a one-time-use-only thing, which makes them
+      fail if you create a seconde account or retry the creation of the
+      first account with the same EAB.
+    - The directive 'MDCertificateAuthority' now checks if its parameter
+      is a http/https url or one of a set of known names. Those are
+      'LetsEncrypt', 'LetsEncrypt-Test', 'Buypass' and 'Buypass-Test'
+      for now and they are not case-sensitive.
+      The default of LetsEncrypt is unchanged.
+    - `MDContactEmail` can now be specified inside a `<MDomain dnsname>`
+      section.
+    - Treating 401 HTTP status codes for orders like 403, since some ACME
+      servers seem to prefer that for accessing oders from other accounts.
+    - When retrieving certificate chains, try to read the repsonse even
+      if the HTTP Content-Type is unrecognized.
+    - Fixed a bug that reset the error counter of a certificate renewal
+      and prevented the increasing delays in further attempts.
+    - Fixed the renewal process giving up every time on an already existing
+      order with some invalid domains. Now, if such are seen in a previous
+      order, a new order is created for a clean start over again.
+      See <https://github.com/icing/mod_md/issues/268>
+    - Fixed a mixup in md-status handler when static certificate files
+      and renewal was configured at the same time.
+
diff --git a/changes-entries/md_2.4.9.txt b/changes-entries/md_2.4.9.txt
new file mode 100644 (file)
index 0000000..27cc5c9
--- /dev/null
@@ -0,0 +1,6 @@
+  *) mod_md: values for External Account Binding (EAB) can
+     now also be configured to be read from a separate JSON
+     file. This allows to keep server configuration permissions
+     world readable without exposing secrets.
+     [Stefan Eissing]
+
diff --git a/changes-entries/pr65620.txt b/changes-entries/pr65620.txt
new file mode 100644 (file)
index 0000000..c14f275
--- /dev/null
@@ -0,0 +1,2 @@
+  *) mod_md: Fix memory leak in case of failures to load the private key.
+     PR 65620 [ Filipe Casal <filipe.casal@trailofbits.com> ]
index 6fcd9b75568b1b98810b2ae189288b8bd2b49e1d..1d72e23e2d6128d05b3da900e0fa1da8f6b6b721 100644 (file)
@@ -1292,4 +1292,47 @@ MDMessageCmd /etc/apache/md-message
         </usage>
     </directivesynopsis>
     
+    <directivesynopsis>
+        <name>MDExternalAccountBinding</name>
+        <description></description>
+        <syntax>MDExternalAccountBinding <var>key-id</var> <var>hmac-64</var> | none | <var>file</var></syntax>
+        <default>MDExternalAccountBinding none</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>
+                Configure values for ACME "External Account Binding", a feature
+                of the ACME standard that allows clients to bind registrations
+                to an existing customer account on ACME servers.
+            </p>
+            <p>
+                Let's Encrypt does not require those, but other ACME CAs do.
+                Check with your ACME CA if you need those and how to obtain the
+                values. They are two strings, a key identifier and a base64 encoded
+                'hmac' value.
+            </p>
+            <p>
+                You can configure those globally or for a specific MDomain. Since
+                these values allow anyone to register under the same account, it is
+                adivsable to give the configuration file restricted permissions,
+                e.g. root only.
+            </p>
+            <p>
+                The value can also be taken from a JSON file, to keep more open
+                permissions on the server configuration and restrict the ones on that
+                file. The JSON itself is:
+            </p>
+            <example><title>EAB JSON Example file</title>
+                <highlight language="config">
+{"kid": "kid-1", "hmac": "zWND..."}
+                </highlight>
+            </example>
+            <p>
+                If you change EAB values, the new ones will be used when the next
+                certificate renewal is due.
+            </p>
+        </usage>
+    </directivesynopsis>
+
 </modulesynopsis>
index 78b7ef863be90c4c7a54db89725eda08539733d1..c7ee08f92d41acb8d003f85f17ecf3c829cdbbd4 100644 (file)
@@ -90,12 +90,15 @@ struct md_t {
     const char *ca_url;             /* url of CA certificate service */
     const char *ca_proto;           /* protocol used vs CA (e.g. ACME) */
     const char *ca_account;         /* account used at CA */
-    const char *ca_agreement;       /* accepted agreement uri between CA and user */ 
+    const char *ca_agreement;       /* accepted agreement uri between CA and user */
     struct apr_array_header_t *ca_challenges; /* challenge types configured for this MD */
     struct apr_array_header_t *cert_files; /* != NULL iff pubcerts explicitly configured */
     struct apr_array_header_t *pkey_files; /* != NULL iff privkeys explicitly configured */
-    
+    const char *ca_eab_kid;         /* optional KEYID for external account binding */
+    const char *ca_eab_hmac;        /* optional HMAC for external accont binding */
+
     md_state_t state;               /* state of this MD */
+    const char *state_descr;        /* description of state of NULL */
     
     struct apr_array_header_t *acme_tls_1_domains; /* domains supporting "acme-tls/1" protocol */
     int stapling;                   /* if OCSP stapling is enabled */
@@ -133,6 +136,8 @@ struct md_t {
 #define MD_KEY_DIR              "dir"
 #define MD_KEY_DOMAIN           "domain"
 #define MD_KEY_DOMAINS          "domains"
+#define MD_KEY_EAB              "eab"
+#define MD_KEY_EAB_REQUIRED     "externalAccountRequired"
 #define MD_KEY_ENTRIES          "entries"
 #define MD_KEY_ERRORED          "errored"
 #define MD_KEY_ERROR            "error"
@@ -142,11 +147,13 @@ struct md_t {
 #define MD_KEY_FINISHED         "finished"
 #define MD_KEY_FROM             "from"
 #define MD_KEY_GOOD             "good"
+#define MD_KEY_HMAC             "hmac"
 #define MD_KEY_HTTP             "http"
 #define MD_KEY_HTTPS            "https"
 #define MD_KEY_ID               "id"
 #define MD_KEY_IDENTIFIER       "identifier"
 #define MD_KEY_KEY              "key"
+#define MD_KEY_KID              "kid"
 #define MD_KEY_KEYAUTHZ         "keyAuthorization"
 #define MD_KEY_LAST             "last"
 #define MD_KEY_LAST_RUN         "last-run"
@@ -183,10 +190,12 @@ struct md_t {
 #define MD_KEY_SHA256_FINGERPRINT  "sha256-fingerprint"
 #define MD_KEY_STAPLING         "stapling"
 #define MD_KEY_STATE            "state"
+#define MD_KEY_STATE_DESCR      "state-descr"
 #define MD_KEY_STATUS           "status"
 #define MD_KEY_STORE            "store"
 #define MD_KEY_SUBPROBLEMS      "subproblems"
 #define MD_KEY_TEMPORARY        "temporary"
+#define MD_KEY_TOS              "termsOfService"
 #define MD_KEY_TOKEN            "token"
 #define MD_KEY_TOTAL            "total"
 #define MD_KEY_TRANSITIVE       "transitive"
@@ -280,20 +289,21 @@ md_t *md_copy(apr_pool_t *p, const md_t *src);
  *
  * This reads and writes the following information: name, domains, ca_url, ca_proto and state.
  */
-struct md_json_t *md_to_json (const md_t *md, apr_pool_t *p);
+struct md_json_t *md_to_json(const md_t *md, apr_pool_t *p);
 md_t *md_from_json(struct md_json_t *json, apr_pool_t *p);
 
+/**
+ * Same as md_to_json(), but with sensitive fields stripped.
+ */
+struct md_json_t *md_to_public_json(const md_t *md, apr_pool_t *p);
+
 int md_is_covered_by_alt_names(const md_t *md, const struct apr_array_header_t* alt_names);
 
 /* how many certificates this domain has/will eventually have. */
 int md_cert_count(const md_t *md);
 
-#define LE_ACMEv1_PROD      "https://acme-v01.api.letsencrypt.org/directory"
-#define LE_ACMEv1_STAGING   "https://acme-staging.api.letsencrypt.org/directory"
-
-#define LE_ACMEv2_PROD      "https://acme-v02.api.letsencrypt.org/directory"  
-#define LE_ACMEv2_STAGING   "https://acme-staging-v02.api.letsencrypt.org/directory"
-
+const char *md_get_ca_name_from_url(apr_pool_t *p, const char *url);
+apr_status_t md_get_ca_url_from_name(const char **purl, apr_pool_t *p, const char *name);
 
 /**************************************************************************************************/
 /* notifications */
index 82e16125a4c59211cc92ecba4c8cc1dd692a05ea..d22aa83ff9c7f489e40c1706d6d10c9341144c2e 100644 (file)
@@ -52,6 +52,7 @@ static acme_problem_status_t Problems[] = {
     { "acme:error:badCSR",                       APR_EINVAL,   1 },
     { "acme:error:badNonce",                     APR_EAGAIN,   0 },
     { "acme:error:badSignatureAlgorithm",        APR_EINVAL,   1 },
+    { "acme:error:externalAccountRequired",      APR_EINVAL,   1 },
     { "acme:error:invalidContact",               APR_BADARG,   1 },
     { "acme:error:unsupportedContact",           APR_EGENERAL, 1 },
     { "acme:error:malformed",                    APR_EINVAL,   1 },
@@ -147,11 +148,7 @@ static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, co
     req->p = pool;
     req->method = method;
     req->url = url;
-    req->prot_hdrs = apr_table_make(pool, 5);
-    if (!req->prot_hdrs) {
-        apr_pool_destroy(pool);
-        return NULL;
-    }
+    req->prot_fields = md_json_create(pool);
     req->max_retries = acme->max_retries;
     req->result = md_result_make(req->p, APR_SUCCESS);
     return req;
@@ -207,6 +204,7 @@ static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t
     switch (res->status) {
         case 400:
             return APR_EINVAL;
+        case 401: /* sectigo returns this instead of 403 */
         case 403:
             return APR_EACCES;
         case 404:
@@ -246,7 +244,7 @@ static apr_status_t acmev2_req_init(md_acme_req_t *req, md_json_t *jpayload)
     md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->p, 
                   "acme payload(len=%" APR_SIZE_T_FMT "): %s", payload.len, payload.data);
     return md_jws_sign(&req->req_json, req->p, &payload,
-                       req->prot_hdrs, req->acme->acct_key, req->acme->acct->url);
+                       req->prot_fields, req->acme->acct_key, req->acme->acct->url);
 }
 
 apr_status_t md_acme_req_body_init(md_acme_req_t *req, md_json_t *payload)
@@ -377,9 +375,9 @@ static apr_status_t md_acme_req_send(md_acme_req_t *req)
                           "error retrieving new nonce from ACME server");
             goto leave;
         }
-        
-        apr_table_set(req->prot_hdrs, "nonce", acme->nonce);
-        apr_table_set(req->prot_hdrs, "url", req->url);
+
+        md_json_sets(acme->nonce, req->prot_fields, "nonce", NULL);
+        md_json_sets(req->url, req->prot_fields, "url", NULL);
         acme->nonce = NULL;
     }
     
@@ -389,14 +387,13 @@ static apr_status_t md_acme_req_send(md_acme_req_t *req)
     if (req->req_json) {
         body = apr_pcalloc(req->p, sizeof(*body));
         body->data = md_json_writep(req->req_json, req->p, MD_JSON_FMT_INDENT);
-        if (!body) {
-            rv = APR_EINVAL; goto leave;
-        }
         body->len = strlen(body->data);
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->p,
+                      "sending JSON body: %s", body->data);
     }
 
-    if (body && md_log_is_level(req->p, MD_LOG_TRACE2)) {
-        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, req->p, 
+    if (body && md_log_is_level(req->p, MD_LOG_TRACE4)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->p,
                       "req: %s %s, body:\n%s", req->method, req->url, body->data);
     }
     else {
@@ -549,10 +546,35 @@ apr_status_t md_acme_use_acct(md_acme_t *acme, md_store_t *store,
     md_acme_acct_t *acct;
     md_pkey_t *pkey;
     apr_status_t rv;
-    
-    if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey, 
+
+    if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey,
+                                               store, MD_SG_ACCOUNTS, acct_id, acme->p))) {
+        if (md_acme_acct_matches_url(acct, acme->url)) {
+            acme->acct_id = apr_pstrdup(p, acct_id);
+            acme->acct = acct;
+            acme->acct_key = pkey;
+            rv = md_acme_acct_validate(acme, store, p);
+        }
+        else {
+            /* account is from another server or, more likely, from another
+             * protocol endpoint on the same server */
+            rv = APR_ENOENT;
+        }
+    }
+    return rv;
+}
+
+apr_status_t md_acme_use_acct_for_md(md_acme_t *acme, struct md_store_t *store,
+                                     apr_pool_t *p, const char *acct_id,
+                                     const md_t *md)
+{
+    md_acme_acct_t *acct;
+    md_pkey_t *pkey;
+    apr_status_t rv;
+
+    if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey,
                                                store, MD_SG_ACCOUNTS, acct_id, acme->p))) {
-        if (acct->ca_url && !strcmp(acct->ca_url, acme->url)) {
+        if (md_acme_acct_matches_md(acct, md)) {
             acme->acct_id = apr_pstrdup(p, acct_id);
             acme->acct = acct;
             acme->acct_key = pkey;
@@ -701,7 +723,8 @@ static apr_status_t update_directory(const md_http_response_t *res, void *data)
             && acme->api.v2.new_nonce) {
             acme->version = MD_ACME_VERSION_2;
         }
-        acme->ca_agreement = md_json_dups(acme->p, json, "meta", "termsOfService", NULL);
+        acme->ca_agreement = md_json_dups(acme->p, json, "meta", MD_KEY_TOS, NULL);
+        acme->eab_required = md_json_getb(json, "meta", MD_KEY_EAB_REQUIRED, NULL);
         acme->new_nonce_fn = acmev2_new_nonce;
         acme->req_init_fn = acmev2_req_init;
         acme->post_new_account_fn = acmev2_POST_new_account;
index 004e7867ea476ba3b5ed4933177185c3de288c06..f28f2b6c617f6bbe890e880e3ab691ce10b81782 100644 (file)
@@ -122,6 +122,7 @@ struct md_acme_t {
     } api;
     const char *ca_agreement;
     const char *acct_name;
+    int eab_required;
     
     md_acme_new_nonce_fn *new_nonce_fn;
     md_acme_req_init_fn *req_init_fn;
@@ -185,11 +186,28 @@ const char *md_acme_acct_url_get(md_acme_t *acme);
 
 /** 
  * Specify the account to use by name in local store. On success, the account
- * the "current" one used by the acme instance.
+ * is the "current" one used by the acme instance.
+ * @param acme the acme instance to set the account for
+ * @param store the store to load accounts from
+ * @param p pool for allocations
+ * @param acct_id name of the account to load
  */
 apr_status_t md_acme_use_acct(md_acme_t *acme, struct md_store_t *store, 
                               apr_pool_t *p, const char *acct_id);
 
+/**
+ * Specify the account to use for a specific MD by name in local store.
+ * On success, the account is the "current" one used by the acme instance.
+ * @param acme the acme instance to set the account for
+ * @param store the store to load accounts from
+ * @param p pool for allocations
+ * @param acct_id name of the account to load
+ * @param md the MD the account shall be used for
+ */
+apr_status_t md_acme_use_acct_for_md(md_acme_t *acme, struct md_store_t *store,
+                                     apr_pool_t *p, const char *acct_id,
+                                     const md_t *md);
+
 /**
  * Get the local name of the account currently used by the acme instance.
  * Will be NULL if no account has been setup successfully.
@@ -232,7 +250,7 @@ struct md_acme_req_t {
     
     const char *url;               /* url to POST the request to */
     const char *method;            /* HTTP method to use */
-    apr_table_t *prot_hdrs;        /* JWS headers needing protection (nonce) */
+    struct md_json_t *prot_fields; /* JWS protected fields */
     struct md_json_t *req_json;    /* JSON to be POSTed in request body */
 
     apr_table_t *resp_hdrs;        /* HTTP response headers */
index 3c0f4536059d5e9824601a508ecaab5faa456de5..94dd83190dcae35047933e37c20d06fe011f9390 100644 (file)
@@ -30,6 +30,7 @@
 #include "md_json.h"
 #include "md_jws.h"
 #include "md_log.h"
+#include "md_result.h"
 #include "md_store.h"
 #include "md_util.h"
 #include "md_version.h"
@@ -43,7 +44,6 @@ static apr_status_t acct_make(md_acme_acct_t **pacct, apr_pool_t *p,
     md_acme_acct_t *acct;
     
     acct = apr_pcalloc(p, sizeof(*acct));
-
     acct->ca_url = ca_url;
     if (!contacts || apr_is_empty_array(contacts)) {
         acct->contacts = apr_array_make(p, 5, sizeof(const char *));
@@ -114,7 +114,9 @@ md_json_t *md_acme_acct_to_json(md_acme_acct_t *acct, apr_pool_t *p)
     if (acct->registration) md_json_setj(acct->registration, jacct, MD_KEY_REGISTRATION, NULL);
     if (acct->agreement) md_json_sets(acct->agreement, jacct, MD_KEY_AGREEMENT, NULL);
     if (acct->orders) md_json_sets(acct->orders, jacct, MD_KEY_ORDERS, NULL);
-    
+    if (acct->eab_kid) md_json_sets(acct->eab_kid, jacct, MD_KEY_EAB, MD_KEY_KID, NULL);
+    if (acct->eab_hmac) md_json_sets(acct->eab_hmac, jacct, MD_KEY_EAB, MD_KEY_HMAC, NULL);
+
     return jacct;
 }
 
@@ -129,22 +131,17 @@ apr_status_t md_acme_acct_from_json(md_acme_acct_t **pacct, md_json_t *json, apr
     if (md_json_has_key(json, MD_KEY_STATUS, NULL)) {
         status = acct_st_from_str(md_json_gets(json, MD_KEY_STATUS, NULL));
     }
-    else {
-        /* old accounts only had disabled boolean field */
-        status = md_json_getb(json, MD_KEY_DISABLED, NULL)? 
-            MD_ACME_ACCT_ST_DEACTIVATED : MD_ACME_ACCT_ST_VALID;
-    }
-    
+
     url = md_json_gets(json, MD_KEY_URL, NULL);
     if (!url) {
         md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no url");
-        goto out;
+        goto leave;
     }
 
     ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL);
     if (!ca_url) {
         md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no CA url: %s", url);
-        goto out;
+        goto leave;
     }
     
     contacts = apr_array_make(p, 5, sizeof(const char *));
@@ -155,14 +152,23 @@ apr_status_t md_acme_acct_from_json(md_acme_acct_t **pacct, md_json_t *json, apr
         md_json_getsa(contacts, json, MD_KEY_REGISTRATION, MD_KEY_CONTACT, NULL);
     }
     rv = acct_make(&acct, p, ca_url, contacts);
-    if (APR_SUCCESS == rv) {
-        acct->status = status;
-        acct->url = url;
+    if (APR_SUCCESS != rv) goto leave;
+
+    acct->status = status;
+    acct->url = url;
+    acct->agreement = md_json_gets(json, MD_KEY_AGREEMENT, NULL);
+    if (!acct->agreement) {
+        /* backward compatible check */
         acct->agreement = md_json_gets(json, "terms-of-service", NULL);
-        acct->orders = md_json_gets(json, MD_KEY_ORDERS, NULL);
+    }
+    acct->orders = md_json_gets(json, MD_KEY_ORDERS, NULL);
+    if (md_json_has_key(json, MD_KEY_EAB, MD_KEY_KID, NULL)
+        && md_json_has_key(json, MD_KEY_EAB, MD_KEY_HMAC, NULL)) {
+        acct->eab_kid = md_json_gets(json, MD_KEY_EAB, MD_KEY_KID, NULL);
+        acct->eab_hmac = md_json_gets(json, MD_KEY_EAB, MD_KEY_HMAC, NULL);
     }
 
-out:
+leave:
     *pacct = (APR_SUCCESS == rv)? acct : NULL;
     return rv;
 }
@@ -228,10 +234,36 @@ out:
 /**************************************************************************************************/
 /* Lookup */
 
+int md_acme_acct_matches_url(md_acme_acct_t *acct, const char *url)
+{
+    /* The ACME url must match exactly */
+    if (!url || !acct->ca_url || strcmp(acct->ca_url, url)) return 0;
+    return 1;
+}
+
+int md_acme_acct_matches_md(md_acme_acct_t *acct, const md_t *md)
+{
+    if (!md_acme_acct_matches_url(acct, md->ca_url)) return 0;
+    /* if eab values are not mentioned, we match an account regardless
+     * if it was registered with eab or not */
+    if (!md->ca_eab_kid || !md->ca_eab_hmac) {
+        /* No eab only acceptable when no eab is asked for.
+         * This prevents someone that has no external account binding
+         * to re-use an account from another MDomain that was created
+         * with a binding. */
+        return !acct->eab_kid || !acct->eab_hmac;
+    }
+    /* But of eab is asked for, we need an acct that matches exactly.
+     * When someone configures a new EAB and we need
+     * to created a new account for it. */
+    if (!acct->eab_kid || !acct->eab_hmac) return 0;
+    return !strcmp(acct->eab_kid, md->ca_eab_kid)
+           && !strcmp(acct->eab_hmac, md->ca_eab_hmac);
+}
+
 typedef struct {
     apr_pool_t *p;
-    md_acme_t *acme;
-    int url_match;
+    const md_t *md;
     const char *id;
 } find_ctx;
 
@@ -239,50 +271,46 @@ static int find_acct(void *baton, const char *name, const char *aspect,
                      md_store_vtype_t vtype, void *value, apr_pool_t *ptemp)
 {
     find_ctx *ctx = baton;
-    int disabled;
-    const char *ca_url, *status;
-    
+    md_acme_acct_t *acct;
+    apr_status_t rv;
+
     (void)aspect;
     (void)ptemp;
     md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, "account candidate %s/%s", name, aspect); 
     if (MD_SV_JSON == vtype) {
-        md_json_t *json = value;
-        
-        status = md_json_gets(json, MD_KEY_STATUS, NULL);
-        disabled = md_json_getb(json, MD_KEY_DISABLED, NULL);
-        ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL);
-        
-        if ((!status || !strcmp("valid", status)) && !disabled 
-            && (!ctx->url_match || (ca_url && !strcmp(ctx->acme->url, ca_url)))) {
+        rv = md_acme_acct_from_json(&acct, (md_json_t*)value, ptemp);
+        if (APR_SUCCESS != rv) goto cleanup;
+
+        if (MD_ACME_ACCT_ST_VALID == acct->status
+            && (!ctx->md || md_acme_acct_matches_md(acct, ctx->md))) {
             md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, 
-                          "found account %s for %s: %s, status=%s, disabled=%d, ca-url=%s", 
-                          name, ctx->acme->url, aspect, status, disabled, ca_url);
+                          "found account %s for %s: %s, status=%d",
+                          acct->id, ctx->md->ca_url, aspect, acct->status);
             ctx->id = apr_pstrdup(ctx->p, name);
             return 0;
         }
     }
+cleanup:
     return 1;
 }
 
 static apr_status_t acct_find(const char **pid, md_acme_acct_t **pacct, md_pkey_t **ppkey, 
                               md_store_t *store, md_store_group_t group,
-                              const char *name_pattern, int url_match, 
-                              md_acme_t *acme, apr_pool_t *p)
+                              const char *name_pattern,
+                              const md_t *md, apr_pool_t *p)
 {
     apr_status_t rv;
     find_ctx ctx;
-    
+
+    memset(&ctx, 0, sizeof(ctx));
     ctx.p = p;
-    ctx.acme = acme;
-    ctx.id = NULL;
-    ctx.url_match = url_match;
-    *pid = NULL;
-    
+    ctx.md = md;
+
     rv = md_store_iter(find_acct, &ctx, store, p, group, name_pattern, MD_FN_ACCOUNT, MD_SV_JSON);
     if (ctx.id) {
         *pid = ctx.id;
         rv = md_acme_acct_load(pacct, ppkey, store, group, ctx.id, p);
-        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "loading account %s", ctx.id);
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "acct_find: got account %s", ctx.id);
     }
     else {
         *pacct = NULL;
@@ -293,19 +321,26 @@ static apr_status_t acct_find(const char **pid, md_acme_acct_t **pacct, md_pkey_
 }
 
 static apr_status_t acct_find_and_verify(md_store_t *store, md_store_group_t group, 
-                                         const char *name_pattern, md_acme_t *acme, apr_pool_t *p)
+                                         const char *name_pattern,
+                                         md_acme_t *acme, const md_t *md,
+                                         apr_pool_t *p)
 {
     md_acme_acct_t *acct;
     md_pkey_t *pkey;
     const char *id;
     apr_status_t rv;
 
-    if (APR_SUCCESS == (rv = acct_find(&id, &acct, &pkey, store, group, name_pattern, 1, acme, p))) {
+    rv = acct_find(&id, &acct, &pkey, store, group, name_pattern, md, p);
+    if (APR_SUCCESS == rv) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "acct_find_and_verify: found %s",
+                      id);
         acme->acct_id = (MD_SG_STAGING == group)? NULL : id;
         acme->acct = acct;
         acme->acct_key = pkey;
-        rv = md_acme_acct_validate(acme, NULL, p);
-    
+        rv = md_acme_acct_validate(acme, (MD_SG_STAGING == group)? NULL : store, p);
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p, "acct_find_and_verify: verified %s",
+                      id);
+
         if (APR_SUCCESS != rv) {
             acme->acct_id = NULL;
             acme->acct = NULL;
@@ -320,13 +355,13 @@ static apr_status_t acct_find_and_verify(md_store_t *store, md_store_group_t gro
     return rv;
 }
 
-apr_status_t md_acme_find_acct(md_acme_t *acme, md_store_t *store)
+apr_status_t md_acme_find_acct_for_md(md_acme_t *acme, md_store_t *store, const md_t *md)
 {
     apr_status_t rv;
     
     while (APR_EAGAIN == (rv = acct_find_and_verify(store, MD_SG_ACCOUNTS, 
                                                     mk_acct_pattern(acme->p, acme), 
-                                                    acme, acme->p))) {
+                                                    acme, md, acme->p))) {
         /* nop */
     }
     
@@ -335,61 +370,31 @@ apr_status_t md_acme_find_acct(md_acme_t *acme, md_store_t *store)
          * can already be found in MD_SG_STAGING? */
         md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, 
                       "no account found, looking in STAGING");
-        while (APR_EAGAIN == (rv = acct_find_and_verify(store, MD_SG_STAGING, "*", 
-                                                        acme, acme->p))) {
-            /* nop */
+        rv = acct_find_and_verify(store, MD_SG_STAGING, "*", acme, md, acme->p);
+        if (APR_EAGAIN == rv) {
+            rv = APR_ENOENT;
         }
     }
     return rv;
 }
 
-typedef struct {
-    apr_pool_t *p;
-    const char *url;
-    const char *id;
-} load_ctx;
-
-static int id_by_url(void *baton, const char *name, const char *aspect,
-                     md_store_vtype_t vtype, void *value, apr_pool_t *ptemp)
-{
-    load_ctx *ctx = baton;
-    int disabled;
-    const char *acct_url, *status;
-    
-    (void)aspect;
-    (void)ptemp;
-    if (MD_SV_JSON == vtype) {
-        md_json_t *json = value;
-        
-        status = md_json_gets(json, MD_KEY_STATUS, NULL);
-        disabled = md_json_getb(json, MD_KEY_DISABLED, NULL);
-        acct_url = md_json_gets(json, MD_KEY_URL, NULL);
-        
-        if ((!status || !strcmp("valid", status)) && !disabled 
-            && acct_url && !strcmp(ctx->url, acct_url)) {
-            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, 
-                          "found account %s for url %s: %s, status=%s, disabled=%d", 
-                          name, ctx->url, aspect, status, disabled);
-            ctx->id = apr_pstrdup(ctx->p, name);
-            return 0;
-        }
-    }
-    return 1;
-}
-
-apr_status_t md_acme_acct_id_for_url(const char **pid, md_store_t *store, 
-                                     md_store_group_t group, const char *url, apr_pool_t *p)
+apr_status_t md_acme_acct_id_for_md(const char **pid, md_store_t *store,
+                                    md_store_group_t group, const md_t *md,
+                                    apr_pool_t *p)
 {
     apr_status_t rv;
-    load_ctx ctx;
-    
+    find_ctx ctx;
+
+    memset(&ctx, 0, sizeof(ctx));
     ctx.p = p;
-    ctx.url = url;
-    ctx.id = NULL;
-    
-    rv = md_store_iter(id_by_url, &ctx, store, p, group, "*", MD_FN_ACCOUNT, MD_SV_JSON);
-    *pid = (APR_SUCCESS == rv)? ctx.id : NULL;
-    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "acct_id_by_url %s -> %s", url, *pid);
+    ctx.md = md;
+
+    rv = md_store_iter(find_acct, &ctx, store, p, group, "*", MD_FN_ACCOUNT, MD_SV_JSON);
+    if (ctx.id) {
+        *pid = ctx.id;
+        rv = APR_SUCCESS;
+    }
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "acct_id_for_md %s -> %s", md->name, *pid);
     return rv;
 }
 
@@ -399,6 +404,8 @@ typedef struct {
     md_acme_t *acme;
     apr_pool_t *p;
     const char *agreement;
+    const char *eab_kid;
+    const char *eab_hmac;
 } acct_ctx_t;
 
 /**************************************************************************************************/
@@ -416,7 +423,12 @@ static apr_status_t acct_upd(md_acme_t *acme, apr_pool_t *p,
     acct_ctx_t *ctx = baton;
     apr_status_t rv = APR_SUCCESS;
     md_acme_acct_t *acct = acme->acct;
-    
+
+    if (md_log_is_level(p, MD_LOG_TRACE2)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, acme->p, "acct update response: %s",
+                      md_json_writep(body, p, MD_JSON_FMT_COMPACT));
+    }
+
     if (!acct->url) {
         const char *location = apr_table_get(hdrs, "location");
         if (!location) {
@@ -437,6 +449,10 @@ static apr_status_t acct_upd(md_acme_t *acme, apr_pool_t *p,
     if (md_json_has_key(body, MD_KEY_ORDERS, NULL)) {
         acct->orders = md_json_dups(acme->p, body, MD_KEY_ORDERS, NULL);
     }
+    if (ctx->eab_kid && ctx->eab_hmac) {
+        acct->eab_kid = ctx->eab_kid;
+        acct->eab_hmac = ctx->eab_hmac;
+    }
     acct->registration = md_json_clone(ctx->p, body);
     
     md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "updated acct %s", acct->url);
@@ -451,6 +467,7 @@ apr_status_t md_acme_acct_update(md_acme_t *acme)
     if (!acme->acct) {
         return APR_EINVAL;
     }
+    memset(&ctx, 0, sizeof(ctx));
     ctx.acme = acme;
     ctx.p = acme->p;
     return md_acme_POST(acme, acme->acct->url, on_init_acct_upd, acct_upd, NULL, NULL, &ctx);
@@ -461,7 +478,16 @@ apr_status_t md_acme_acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_
     apr_status_t rv;
     
     if (APR_SUCCESS != (rv = md_acme_acct_update(acme))) {
-        if (acme->acct && (APR_ENOENT == rv || APR_EACCES == rv)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, acme->p,
+                      "acct update failed for %s", acme->acct->url);
+        if (APR_EINVAL == rv && (acme->acct->agreement || !acme->ca_agreement)) {
+            /* Sadly, some proprietary ACME servers choke on empty POSTs
+             * on accounts. Try a faked ToS agreement. */
+            md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, acme->p,
+                          "trying acct update via ToS agreement");
+            rv = md_acme_agree(acme, p, "accepted");
+        }
+        if (acme->acct && (APR_ENOENT == rv || APR_EACCES == rv || APR_EINVAL == rv)) {
             if (MD_ACME_ACCT_ST_VALID == acme->acct->status) {
                 acme->acct->status = MD_ACME_ACCT_ST_UNKNOWN;
                 if (store) {
@@ -479,21 +505,73 @@ apr_status_t md_acme_acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_
 /**************************************************************************************************/
 /* Register a new account */
 
+static apr_status_t get_eab(md_json_t **peab, md_acme_req_t *req, const char *kid,
+                            const char *hmac64, md_pkey_t *account_key,
+                            const char *url)
+{
+    md_json_t *eab, *prot_fields, *jwk;
+    md_data_t payload, hmac_key;
+    apr_status_t rv;
+
+    prot_fields = md_json_create(req->p);
+    md_json_sets(url, prot_fields, "url", NULL);
+    md_json_sets(kid, prot_fields, "kid", NULL);
+
+    rv = md_jws_get_jwk(&jwk, req->p, account_key);
+    if (APR_SUCCESS != rv) goto cleanup;
+
+    md_data_null(&payload);
+    payload.data = md_json_writep(jwk, req->p, MD_JSON_FMT_COMPACT);
+    if (!payload.data) {
+        rv = APR_EINVAL;
+        goto cleanup;
+    }
+    payload.len = strlen(payload.data);
+
+    md_util_base64url_decode(&hmac_key, hmac64, req->p);
+    if (!hmac_key.len) {
+        rv = APR_EINVAL;
+        md_result_problem_set(req->result, rv, "apache:eab-hmac-invalid",
+                              "external account binding HMAC value is not valid base64", NULL);
+        goto cleanup;
+    }
+
+    rv = md_jws_hmac(&eab, req->p, &payload, prot_fields, &hmac_key);
+    if (APR_SUCCESS != rv) {
+        md_result_problem_set(req->result, rv, "apache:eab-hmac-fail",
+                              "external account binding MAC could not be computed", NULL);
+    }
+
+cleanup:
+    *peab = (APR_SUCCESS == rv)? eab : NULL;
+    return rv;
+}
+
 static apr_status_t on_init_acct_new(md_acme_req_t *req, void *baton)
 {
     acct_ctx_t *ctx = baton;
-    md_json_t *jpayload;
+    md_json_t *jpayload, *jeab;
+    apr_status_t rv;
 
     jpayload = md_json_create(req->p);
     md_json_setsa(ctx->acme->acct->contacts, jpayload, MD_KEY_CONTACT, NULL);
     if (ctx->agreement) {
         md_json_setb(1, jpayload, "termsOfServiceAgreed", NULL);
     }
-    return md_acme_req_body_init(req, jpayload);
+    if (ctx->eab_kid && ctx->eab_hmac) {
+        rv = get_eab(&jeab, req, ctx->eab_kid, ctx->eab_hmac,
+                     req->acme->acct_key, req->url);
+        if (APR_SUCCESS != rv) goto cleanup;
+        md_json_setj(jeab, jpayload, "externalAccountBinding", NULL);
+    }
+    rv = md_acme_req_body_init(req, jpayload);
+
+cleanup:
+    return rv;
 } 
 
-apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store, apr_pool_t *p, 
-                                   apr_array_header_t *contacts, const char *agreement)
+apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store,
+                                   const md_t *md, apr_pool_t *p)
 {
     apr_status_t rv;
     md_pkey_t *pkey;
@@ -503,15 +581,17 @@ apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store, apr_pool_
     acct_ctx_t ctx;
     
     md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "create new account");
-    
+
+    memset(&ctx, 0, sizeof(ctx));
     ctx.acme = acme;
     ctx.p = p;
     /* The agreement URL is submitted when the ACME server announces Terms-of-Service
      * in its directory meta data. The magic value "accepted" will always use the
      * advertised URL. */
     ctx.agreement = NULL;
-    if (acme->ca_agreement && agreement) {
-        ctx.agreement = !strcmp("accepted", agreement)? acme->ca_agreement : agreement;
+    if (acme->ca_agreement && md->ca_agreement) {
+        ctx.agreement = !strcmp("accepted", md->ca_agreement)?
+            acme->ca_agreement : md->ca_agreement;
     }
     
     if (ctx.agreement) {
@@ -521,9 +601,11 @@ apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store, apr_pool_
             goto out;
         }
     }
+    ctx.eab_kid = md->ca_eab_kid;
+    ctx.eab_hmac = md->ca_eab_hmac;
     
-    for (i = 0; i < contacts->nelts; ++i) {
-        uri = APR_ARRAY_IDX(contacts, i, const char *);
+    for (i = 0; i < md->contacts->nelts; ++i) {
+        uri = APR_ARRAY_IDX(md->contacts, i, const char *);
         if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, uri, &err))) {
             md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, 
                           "invalid contact uri (%s): %s", err, uri);
@@ -540,19 +622,19 @@ apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store, apr_pool_
      */
     if (!acme->acct_key) {
         find_ctx fctx;
-    
+
+        memset(&fctx, 0, sizeof(fctx));
         fctx.p = p;
-        fctx.acme = acme;
-        fctx.id = NULL;
-        fctx.url_match = 0;
-        
+        fctx.md = md;
+
         md_store_iter(find_acct, &fctx, store, p, MD_SG_ACCOUNTS, 
                       mk_acct_pattern(p, acme), MD_FN_ACCOUNT, MD_SV_JSON);
         if (fctx.id) {
-            rv = md_store_load(store, MD_SG_ACCOUNTS, fctx.id, MD_FN_ACCT_KEY, MD_SV_PKEY, 
+            rv = md_store_load(store, MD_SG_ACCOUNTS, fctx.id, MD_FN_ACCT_KEY, MD_SV_PKEY,
                                (void**)&acme->acct_key, p);
             if (APR_SUCCESS == rv) {
-                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "reusing key from account %s", fctx.id);
+                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+                              "reusing key from account %s", fctx.id);
             }
             else {
                 acme->acct_key = NULL;
@@ -570,7 +652,7 @@ apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store, apr_pool_
         md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "created new account key");
     }
     
-    if (APR_SUCCESS != (rv = acct_make(&acme->acct,  p, acme->url, contacts))) goto out;
+    if (APR_SUCCESS != (rv = acct_make(&acme->acct,  p, acme->url, md->contacts))) goto out;
     rv = md_acme_POST_new_account(acme,  on_init_acct_new, acct_upd, NULL, NULL, &ctx);
     if (APR_SUCCESS == rv) {
         md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, 
@@ -608,6 +690,7 @@ apr_status_t md_acme_acct_deactivate(md_acme_t *acme, apr_pool_t *p)
     }
     md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "delete account %s from %s", 
                   acct->url, acct->ca_url);
+    memset(&ctx, 0, sizeof(ctx));
     ctx.acme = acme;
     ctx.p = p;
     return md_acme_POST(acme, acct->url, on_init_acct_del, acct_upd, NULL, NULL, &ctx);
@@ -637,6 +720,7 @@ apr_status_t md_acme_agree(md_acme_t *acme, apr_pool_t *p, const char *agreement
         acme->acct->agreement = acme->ca_agreement;
     }
     
+    memset(&ctx, 0, sizeof(ctx));
     ctx.acme = acme;
     ctx.p = p;
     return md_acme_POST(acme, acme->acct->url, on_init_agree_tos, acct_upd, NULL, NULL, &ctx);
index c2cf64c7e48980e8b818b9f257acc9b6220e78fe..b5bba631f4d4169de6b5601b4ed65c3cb98a380e 100644 (file)
@@ -44,6 +44,8 @@ struct md_acme_acct_t {
     const char *tos_required;       /* terms of service asked for by CA */
     const char *agreement;          /* terms of service agreed to by user */
     const char *orders;             /* URL where certificate orders are found (ACMEv2) */
+    const char *eab_kid;            /* external account binding keyid used or NULL */
+    const char *eab_hmac;           /* external account binding hmac used or NULL */
     struct md_json_t *registration; /* data from server registration */
 };
 
@@ -104,21 +106,20 @@ const char *md_acme_get_agreement(md_acme_t *acme);
  * Find an existing account in the local store. On APR_SUCCESS, the acme
  * instance will have a current, validated account to use.
  */ 
-apr_status_t md_acme_find_acct(md_acme_t *acme, md_store_t *store);
+apr_status_t md_acme_find_acct_for_md(md_acme_t *acme, md_store_t *store, const md_t *md);
 
 /**
- * Find the account id for a given account url. 
+ * Find the account id for a given md.
  */
-apr_status_t md_acme_acct_id_for_url(const char **pid, md_store_t *store, 
-                                     md_store_group_t group, const char *url, apr_pool_t *p);
+apr_status_t md_acme_acct_id_for_md(const char **pid, md_store_t *store,
+                                    md_store_group_t group, const md_t *md, apr_pool_t *p);
 
 /**
- * Create a new account at the ACME server. The
+ * Create a new account at the ACME server for an MD. The
  * new account is the one used by the acme instance afterwards, on success.
  */
 apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store, 
-                                   apr_pool_t *p, apr_array_header_t *contacts, 
-                                   const char *agreement);
+                                   const md_t *md, apr_pool_t *p);
 
 apr_status_t md_acme_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme,  
                                const char **pid, struct md_acme_acct_t *acct, 
@@ -133,4 +134,15 @@ apr_status_t md_acme_acct_load(struct md_acme_acct_t **pacct, struct md_pkey_t *
                                md_store_t *store, md_store_group_t group, 
                                const char *name, apr_pool_t *p);
 
+/*
+ * Return != 0 iff the account can be used for the ACME url.
+ */
+int md_acme_acct_matches_url(md_acme_acct_t *acct, const char *url);
+
+/*
+ * Return != 0 iff the account can be used for the MD, including
+ * its CA url and EAB settings.
+ */
+int md_acme_acct_matches_md(md_acme_acct_t *acct, const md_t *md);
+
 #endif /* md_acme_acct_h */
index d5632fa6b6101e6910e7096af238cc29ca8d93b7..01431328f1ce8d539e899dc4b29b5a489d6e2647 100644 (file)
@@ -393,7 +393,13 @@ static apr_status_t cha_tls_alpn_01_setup(md_acme_authz_cha_t *cha, md_acme_auth
         /* Raise event that challenge data has been set up before we tell the
            ACME server. Clusters might want to distribute it. */
         event = apr_psprintf(p, "challenge-setup:%s:%s", MD_AUTHZ_TYPE_TLSALPN01, authz->domain);
-        md_result_holler(result, event, p);
+        rv = md_result_raise(result, event, p);
+        if (APR_SUCCESS != rv) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+                          "%s: event '%s' failed. aborting challenge setup",
+                          authz->domain, event);
+            goto out;
+        }
         /* challenge is setup or was changed from previous data, tell ACME server
          * so it may (re)try verification */        
         authz_req_ctx_init(&ctx, acme, NULL, authz, p);
@@ -463,7 +469,13 @@ static apr_status_t cha_dns_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *
     /* Raise event that challenge data has been set up before we tell the
        ACME server. Clusters might want to distribute it. */
     event = apr_psprintf(p, "challenge-setup:%s:%s", MD_AUTHZ_TYPE_DNS01, authz->domain);
-    md_result_holler(result, event, p);
+    rv = md_result_raise(result, event, p);
+    if (APR_SUCCESS != rv) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p,
+                      "%s: event '%s' failed. aborting challenge setup",
+                      authz->domain, event);
+        goto out;
+    }
     /* challenge is setup, tell ACME server so it may (re)try verification */
     md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: dns-01 setup succeeded for %s",
        mdomain, authz->domain);
index 5e3b48cf91e4e6e7ee1cf9e5dde6beb49ed7731a..6c461dc9af337bdaccfa687d4a08ca81364ac588 100644 (file)
 /* account setup */
 
 static apr_status_t use_staged_acct(md_acme_t *acme, struct md_store_t *store, 
-                                    const char *md_name, apr_pool_t *p)
+                                    const md_t *md, apr_pool_t *p)
 {
     md_acme_acct_t *acct;
     md_pkey_t *pkey;
     apr_status_t rv;
     
     if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey, store, 
-                                               MD_SG_STAGING, md_name, acme->p))) {
+                                               MD_SG_STAGING, md->name, acme->p))) {
         acme->acct_id = NULL;
         acme->acct = acct;
         acme->acct_key = pkey;
@@ -89,7 +89,7 @@ apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result)
     md_acme_clear_acct(ad->acme);
     
     /* Do we have a staged (modified) account? */
-    if (APR_SUCCESS == (rv = use_staged_acct(ad->acme, d->store, md->name, d->p))) {
+    if (APR_SUCCESS == (rv = use_staged_acct(ad->acme, d->store, md, d->p))) {
         md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "re-using staged account");
     }
     else if (!APR_STATUS_IS_ENOENT(rv)) {
@@ -99,7 +99,7 @@ apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result)
     /* Get an account for the ACME server for this MD */
     if (!ad->acme->acct && md->ca_account) {
         md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "re-use account '%s'", md->ca_account);
-        rv = md_acme_use_acct(ad->acme, d->store, d->p, md->ca_account);
+        rv = md_acme_use_acct_for_md(ad->acme, d->store, d->p, md->ca_account, md);
         if (APR_STATUS_IS_ENOENT(rv) || APR_STATUS_IS_EINVAL(rv)) {
             md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "rejected %s", md->ca_account);
             md->ca_account = NULL;
@@ -114,7 +114,7 @@ apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result)
         /* Find a local account for server, store at MD */ 
         md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: looking at existing accounts",
                       d->proto->protocol);
-        if (APR_SUCCESS == (rv = md_acme_find_acct(ad->acme, d->store))) {
+        if (APR_SUCCESS == (rv = md_acme_find_acct_for_md(ad->acme, d->store, md))) {
             md->ca_account = md_acme_acct_id_get(ad->acme);
             update_md = 1;
             md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: using account %s (id=%s)",
@@ -153,7 +153,19 @@ apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result)
             goto leave;
         }
     
-        rv = md_acme_acct_register(ad->acme, d->store, d->p, md->contacts, md->ca_agreement);
+        if (ad->acme->eab_required && (!md->ca_eab_kid || !strcmp("none", md->ca_eab_kid))) {
+            md_result_printf(result, APR_EINVAL,
+                  "the CA requires 'External Account Binding' which is not "
+                  "configured. This means you need to obtain a 'Key ID' and a "
+                  "'HMAC' from the CA and configure that using the "
+                  "MDExternalAccountBinding directive in your config. "
+                  "The creation of a new ACME account will most likely fail, "
+                  "but an attempt is made anyway.",
+                  ad->acme->ca_agreement);
+            md_result_log(result, MD_LOG_INFO);
+        }
+
+        rv = md_acme_acct_register(ad->acme, d->store, md, d->p);
         if (APR_SUCCESS != rv) {
             if (APR_SUCCESS != ad->acme->last->status) {
                 md_result_dup(result, ad->acme->last);
@@ -169,11 +181,11 @@ apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result)
     
 leave:
     /* Persist MD changes in STAGING, so we pick them up on next run */
-    if (APR_SUCCESS == rv&& update_md) {
+    if (APR_SUCCESS == rv && update_md) {
         rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0);
     }
     /* Persist account changes in STAGING, so we pick them up on next run */
-    if (APR_SUCCESS == rv&& update_acct) {
+    if (APR_SUCCESS == rv && update_acct) {
         rv = save_acct_staged(ad->acme, d->store, md->name, d->p);
     }
     return rv;
@@ -894,6 +906,7 @@ static apr_status_t acme_preload(md_proto_driver_t *d, md_store_group_t load_gro
     md_credentials_t *creds;
     apr_array_header_t *all_creds;
     struct md_acme_acct_t *acct;
+    const char *id;
     int i;
 
     md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: preload start", name);
@@ -953,7 +966,6 @@ static apr_status_t acme_preload(md_proto_driver_t *d, md_store_group_t load_gro
     
     if (acct) {
         md_acme_t *acme;
-        const char *id = md->ca_account;
 
         /* We may have STAGED the same account several times. This happens when
          * several MDs are renewed at once and need a new account. They will all store
@@ -961,8 +973,9 @@ static apr_status_t acme_preload(md_proto_driver_t *d, md_store_group_t load_gro
          * the same url, we save them all into a single one.
          */
         md_result_activity_setn(result, "saving staged account");
-        if (!id && acct->url) {
-            rv = md_acme_acct_id_for_url(&id, d->store, MD_SG_ACCOUNTS, acct->url, d->p);
+        id = md->ca_account;
+        if (!id) {
+            rv = md_acme_acct_id_for_md(&id, d->store, MD_SG_ACCOUNTS, md, d->p);
             if (APR_STATUS_IS_ENOENT(rv)) {
                 id = NULL;
             }
@@ -983,6 +996,14 @@ static apr_status_t acme_preload(md_proto_driver_t *d, md_store_group_t load_gro
         }
         md->ca_account = id;
     }
+    else if (!md->ca_account) {
+        /* staging reused another account and did not create a new one. find
+         * the account, if it is already there */
+        rv = md_acme_acct_id_for_md(&id, d->store, MD_SG_ACCOUNTS, md, d->p);
+        if (APR_SUCCESS == rv) {
+            md->ca_account = id;
+        }
+    }
     
     md_result_activity_setn(result, "saving staged md/privkey/pubcert");
     if (APR_SUCCESS != (rv = md_save(d->store, d->p, load_group, md, 1))) {
index ee1166be1dabf59f79db349a9eb0c4095a347419..8037c762f6e864f14b21db6333713d0790a0c443 100644 (file)
@@ -397,10 +397,15 @@ static apr_status_t await_valid(void *baton, int attempt)
                                                   ctx->result, ctx->p))) goto out;
     switch (ctx->order->status) {
         case MD_ACME_ORDER_ST_VALID:
+            md_result_set(ctx->result, APR_EINVAL, "ACME server order status is 'valid'.");
             break;
         case MD_ACME_ORDER_ST_PROCESSING:
             rv = APR_EAGAIN;
             break;
+        case MD_ACME_ORDER_ST_INVALID:
+            md_result_set(ctx->result, APR_EINVAL, "ACME server order status is 'invalid'.");
+            rv = APR_EINVAL;
+            break;
         default:
             rv = APR_EINVAL;
             break;
@@ -515,12 +520,10 @@ static apr_status_t check_challenges(void *baton, int attempt)
                     goto leave;
                 case MD_ACME_AUTHZ_S_INVALID:
                     rv = APR_EINVAL;
-                    if (!authz->error_type) {
-                        md_result_printf(ctx->result, rv, 
-                                         "domain authorization for %s failed, CA considers "
-                                         "answer to challenge invalid, no error given", 
-                                         authz->domain);
-                    } 
+                    md_result_printf(ctx->result, rv,
+                                     "domain authorization for %s failed, CA considers "
+                                     "answer to challenge invalid%s.",
+                                     authz->domain, authz->error_type? "" : ", no error given");
                     md_result_log(ctx->result, MD_LOG_ERR);
                     goto leave;
                 default:
index ec945c7e5c351be0fa2b5e85ec946b7ee851e649..868fb56b131c4e7a7136d27bf5c8d6304234a2f8 100644 (file)
@@ -63,8 +63,7 @@ apr_status_t md_acme_order_purge(struct md_store_t *store, apr_pool_t *p,
                                  md_store_group_t group, const char *md_name,
                                  apr_table_t *env);
 
-
-apr_status_t md_acme_order_start_challenges(md_acme_order_t *order, md_acme_t *acme, 
+apr_status_t md_acme_order_start_challenges(md_acme_order_t *order, md_acme_t *acme,
                                             apr_array_header_t *challenge_types,
                                             md_store_t *store, const md_t *md, 
                                             apr_table_t *env, struct md_result_t *result,
index 40add6f56559a0e8970ac695dc72743c39e790ba..abb985c44d612901bece054000b72d4eca5d4800 100644 (file)
@@ -51,7 +51,7 @@
  * Either we have an order stored in the STAGING area, or we need to create a 
  * new one at the ACME server.
  */
-static apr_status_t ad_setup_order(md_proto_driver_t *d, md_result_t *result)
+static apr_status_t ad_setup_order(md_proto_driver_t *d, md_result_t *result, int *pis_new)
 {
     md_acme_driver_t *ad = d->baton;
     apr_status_t rv;
@@ -65,6 +65,7 @@ static apr_status_t ad_setup_order(md_proto_driver_t *d, md_result_t *result)
      * if known AUTHZ resource is not valid, remove, goto 4.1.1
      * if no AUTHZ available, create a new one for the domain, store it
      */
+    if (pis_new) *pis_new = 0;
     rv = md_acme_order_load(d->store, MD_SG_STAGING, md->name, &ad->order, d->p);
     if (APR_SUCCESS == rv) {
         md_result_activity_setn(result, "Loaded order from staging");
@@ -82,7 +83,8 @@ static apr_status_t ad_setup_order(md_proto_driver_t *d, md_result_t *result)
     if (APR_SUCCESS != rv) {
         md_result_set(result, rv, "saving order in staging");
     }
-    
+    if (pis_new) *pis_new = 1;
+
 leave:
     md_acme_report_result(ad->acme, rv, result);
     return rv;
@@ -94,7 +96,8 @@ leave:
 apr_status_t md_acmev2_drive_renew(md_acme_driver_t *ad, md_proto_driver_t *d, md_result_t *result)
 {
     apr_status_t rv = APR_SUCCESS;
-    
+    int is_new_order = 0;
+
     md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: (ACMEv2) need certificate", d->md->name);
     
     /* Chose (or create) and ACME account to use */
@@ -115,27 +118,36 @@ apr_status_t md_acmev2_drive_renew(md_acme_driver_t *ad, md_proto_driver_t *d, m
      *   * COMPLETE: all done, return success
      *   * INVALID and otherwise: fail renewal, delete local order
      */
-    if (APR_SUCCESS != (rv = ad_setup_order(d, result))) {
+    if (APR_SUCCESS != (rv = ad_setup_order(d, result, &is_new_order))) {
         goto leave;
     }
     
     rv = md_acme_order_update(ad->order, ad->acme, result, d->p);
-    if (APR_STATUS_IS_ENOENT(rv)) {
-        /* order is no longer known at the ACME server */
+    if (APR_STATUS_IS_ENOENT(rv)
+        || APR_STATUS_IS_EACCES(rv)
+        || MD_ACME_ORDER_ST_INVALID == ad->order->status) {
+        /* order is invalid or no longer known at the ACME server */
         ad->order = NULL;
         md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md->name, d->env);
     }
     else if (APR_SUCCESS != rv) {
         goto leave;
     }
-    
+
+retry:
     if (!ad->order) {
-        rv = ad_setup_order(d, result);
+        rv = ad_setup_order(d, result, &is_new_order);
         if (APR_SUCCESS != rv) goto leave;
     }
     
     rv = md_acme_order_start_challenges(ad->order, ad->acme, ad->ca_challenges,
                                         d->store, d->md, d->env, result, d->p);
+    if (!is_new_order && APR_STATUS_IS_EINVAL(rv)) {
+        /* found 'invalid' domains in previous order, need to start over */
+        ad->order = NULL;
+        md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md->name, d->env);
+        goto retry;
+    }
     if (APR_SUCCESS != rv) goto leave;
     
     rv = md_acme_order_monitor_authzs(ad->order, ad->acme, d->md,
index 9b696e2b995274d7e09b90f998606902a6cdb342..620b809984ca640cab5d26607ac1cf44dbd17548 100644 (file)
@@ -19,6 +19,7 @@
 
 #include <apr_lib.h>
 #include <apr_strings.h>
+#include <apr_uri.h>
 #include <apr_tables.h>
 #include <apr_time.h>
 #include <apr_date.h>
@@ -277,6 +278,8 @@ md_json_t *md_to_json(const md_t *md, apr_pool_t *p)
             md_json_setj(md_pkeys_spec_to_json(md->pks, p), json, MD_KEY_PKEY, NULL);
         }
         md_json_setl(md->state, json, MD_KEY_STATE, NULL);
+        if (md->state_descr)
+            md_json_sets(md->state_descr, json, MD_KEY_STATE_DESCR, NULL);
         md_json_setl(md->renew_mode, json, MD_KEY_RENEW_MODE, NULL);
         if (md->renew_window)
             md_json_sets(md_timeslice_format(md->renew_window, p), json, MD_KEY_RENEW_WINDOW, NULL);
@@ -302,6 +305,10 @@ md_json_t *md_to_json(const md_t *md, apr_pool_t *p)
         if (md->cert_files) md_json_setsa(md->cert_files, json, MD_KEY_CERT_FILES, NULL);
         if (md->pkey_files) md_json_setsa(md->pkey_files, json, MD_KEY_PKEY_FILES, NULL);
         md_json_setb(md->stapling > 0, json, MD_KEY_STAPLING, NULL);
+        if (md->ca_eab_kid && strcmp("none", md->ca_eab_kid)) {
+            md_json_sets(md->ca_eab_kid, json, MD_KEY_EAB, MD_KEY_KID, NULL);
+            if (md->ca_eab_hmac) md_json_sets(md->ca_eab_hmac, json, MD_KEY_EAB, MD_KEY_HMAC, NULL);
+        }
         return json;
     }
     return NULL;
@@ -323,6 +330,7 @@ md_t *md_from_json(md_json_t *json, apr_pool_t *p)
             md->pks = md_pkeys_spec_from_json(md_json_getj(json, MD_KEY_PKEY, NULL), p);
         }
         md->state = (md_state_t)md_json_getl(json, MD_KEY_STATE, NULL);
+        md->state_descr = md_json_dups(p, json, MD_KEY_STATE_DESCR, NULL);
         if (MD_S_EXPIRED_DEPRECATED == md->state) md->state = MD_S_COMPLETE;
         md->renew_mode = (int)md_json_getl(json, MD_KEY_RENEW_MODE, NULL);
         md->domains = md_array_str_compact(p, md->domains, 0);
@@ -354,8 +362,84 @@ md_t *md_from_json(md_json_t *json, apr_pool_t *p)
         }
         md->stapling = (int)md_json_getb(json, MD_KEY_STAPLING, NULL);
         
+        if (md_json_has_key(json, MD_KEY_EAB, NULL)) {
+            md->ca_eab_kid = md_json_dups(p, json, MD_KEY_EAB, MD_KEY_KID, NULL);
+            md->ca_eab_hmac = md_json_dups(p, json, MD_KEY_EAB, MD_KEY_HMAC, NULL);
+        }
         return md;
     }
     return NULL;
 }
 
+md_json_t *md_to_public_json(const md_t *md, apr_pool_t *p)
+{
+    md_json_t *json = md_to_json(md, p);
+    if (md_json_has_key(json, MD_KEY_EAB, MD_KEY_HMAC, NULL)) {
+        md_json_sets("***", json, MD_KEY_EAB, MD_KEY_HMAC, NULL);
+    }
+    return json;
+}
+
+typedef struct {
+    const char *name;
+    const char *url;
+} md_ca_t;
+
+#define LE_ACMEv2_PROD      "https://acme-v02.api.letsencrypt.org/directory"
+#define LE_ACMEv2_STAGING   "https://acme-staging-v02.api.letsencrypt.org/directory"
+#define BUYPASS_ACME        "https://api.buypass.com/acme/directory"
+#define BUYPASS_ACME_TEST   "https://api.test4.buypass.no/acme/directory"
+
+static md_ca_t KNOWN_CAs[] = {
+    { "LetsEncrypt", LE_ACMEv2_PROD },
+    { "LetsEncrypt-Test", LE_ACMEv2_STAGING },
+    { "Buypass", BUYPASS_ACME },
+    { "Buypass-Test", BUYPASS_ACME_TEST },
+};
+
+const char *md_get_ca_name_from_url(apr_pool_t *p, const char *url)
+{
+    apr_uri_t uri_parsed;
+    unsigned int i;
+
+    for (i = 0; i < sizeof(KNOWN_CAs)/sizeof(KNOWN_CAs[0]); ++i) {
+        if (!apr_strnatcasecmp(KNOWN_CAs[i].url, url)) {
+            return KNOWN_CAs[i].name;
+        }
+    }
+    if (APR_SUCCESS == apr_uri_parse(p, url, &uri_parsed)) {
+        return uri_parsed.hostname;
+    }
+    return apr_pstrdup(p, url);
+}
+
+apr_status_t md_get_ca_url_from_name(const char **purl, apr_pool_t *p, const char *name)
+{
+    const char *err;
+    unsigned int i;
+    apr_status_t rv = APR_SUCCESS;
+
+    *purl = NULL;
+    for (i = 0; i < sizeof(KNOWN_CAs)/sizeof(KNOWN_CAs[0]); ++i) {
+        if (!apr_strnatcasecmp(KNOWN_CAs[i].name, name)) {
+            *purl = KNOWN_CAs[i].url;
+            goto leave;
+        }
+    }
+    *purl = name;
+    rv = md_util_abs_http_uri_check(p, name, &err);
+    if (APR_SUCCESS != rv) {
+        apr_array_header_t *names;
+
+        names = apr_array_make(p, 10, sizeof(const char*));
+        for (i = 0; i < sizeof(KNOWN_CAs)/sizeof(KNOWN_CAs[0]); ++i) {
+            APR_ARRAY_PUSH(names, const char *) = KNOWN_CAs[i].name;
+        }
+        *purl = apr_psprintf(p,
+            "The CA name '%s' is not known and it is not a URL either (%s). "
+            "Known CA names are: %s.",
+            name, err, apr_array_pstrcat(p, names, ' '));
+    }
+leave:
+    return rv;
+}
index 55826be86017b2ee5329bbb08e028465ba3ceabb..a264b867c7676626bfd7863b0a9c1c719e1675cb 100644 (file)
@@ -27,6 +27,7 @@
 
 #include <openssl/err.h>
 #include <openssl/evp.h>
+#include <openssl/hmac.h>
 #include <openssl/pem.h>
 #include <openssl/rand.h>
 #include <openssl/rsa.h>
@@ -643,6 +644,7 @@ static apr_status_t pkey_to_buffer(md_data_t *buf, md_pkey_t *pkey, apr_pool_t *
     const EVP_CIPHER *cipher = NULL;
     pem_password_cb *cb = NULL;
     void *cb_baton = NULL;
+    apr_status_t rv = APR_SUCCESS;
     passwd_ctx ctx;
     unsigned long err;
     int i;
@@ -651,7 +653,8 @@ static apr_status_t pkey_to_buffer(md_data_t *buf, md_pkey_t *pkey, apr_pool_t *
         return APR_ENOMEM;
     }
     if (pass_len > INT_MAX) {
-        return APR_EINVAL;
+        rv = APR_EINVAL;
+        goto cleanup;
     }
     if (pass && pass_len > 0) {
         ctx.pass_phrase = pass;
@@ -660,7 +663,8 @@ static apr_status_t pkey_to_buffer(md_data_t *buf, md_pkey_t *pkey, apr_pool_t *
         cb_baton = &ctx;
         cipher = EVP_aes_256_cbc();
         if (!cipher) {
-            return APR_ENOTIMPL;
+            rv = APR_ENOTIMPL;
+            goto cleanup;
         }
     }
     
@@ -670,11 +674,11 @@ static apr_status_t pkey_to_buffer(md_data_t *buf, md_pkey_t *pkey, apr_pool_t *
 #else 
     if (!PEM_write_bio_PrivateKey(bio, pkey->pkey, cipher, NULL, 0, cb, cb_baton)) {
 #endif
-        BIO_free(bio);
         err = ERR_get_error();
         md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "PEM_write key: %ld %s", 
                       err, ERR_error_string(err, NULL)); 
-        return APR_EINVAL;
+        rv = APR_EINVAL;
+        goto cleanup;
     }
 
     md_data_null(buf);
@@ -684,8 +688,10 @@ static apr_status_t pkey_to_buffer(md_data_t *buf, md_pkey_t *pkey, apr_pool_t *
         i = BIO_read(bio, (char*)buf->data, i);
         buf->len = (apr_size_t)i;
     }
+
+cleanup:
     BIO_free(bio);
-    return APR_SUCCESS;
+    return rv;
 }
 
 apr_status_t md_pkey_fsave(md_pkey_t *pkey, apr_pool_t *p, 
@@ -786,21 +792,25 @@ static apr_status_t gen_ec(md_pkey_t **ppkey, apr_pool_t *p, const char *curve)
 #ifdef NID_secp384r1
     if (NID_undef == curve_nid && !apr_strnatcasecmp("secp384r1", curve)) {
         curve_nid = NID_secp384r1;
+        curve = EC_curve_nid2nist(curve_nid);
     }
 #endif
 #ifdef NID_X9_62_prime256v1
     if (NID_undef == curve_nid && !apr_strnatcasecmp("secp256r1", curve)) {
         curve_nid = NID_X9_62_prime256v1;
+        curve = EC_curve_nid2nist(curve_nid);
     }
 #endif
 #ifdef NID_X9_62_prime192v1
     if (NID_undef == curve_nid && !apr_strnatcasecmp("secp192r1", curve)) {
         curve_nid = NID_X9_62_prime192v1;
+        curve = EC_curve_nid2nist(curve_nid);
     }
 #endif
 #if defined(NID_X25519) && !defined(LIBRESSL_VERSION_NUMBER)
     if (NID_undef == curve_nid && !apr_strnatcasecmp("X25519", curve)) {
         curve_nid = NID_X25519;
+        curve = EC_curve_nid2nist(curve_nid);
     }
 #endif
     if (NID_undef == curve_nid) {
@@ -844,6 +854,7 @@ static apr_status_t gen_ec(md_pkey_t **ppkey, apr_pool_t *p, const char *curve)
 #endif
 
     default:
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
         if (APR_SUCCESS != (rv = check_EC_curve(curve_nid, p))) goto leave;
         if (NULL == (ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL))
             || EVP_PKEY_paramgen_init(ctx) <= 0 
@@ -855,6 +866,17 @@ static apr_status_t gen_ec(md_pkey_t **ppkey, apr_pool_t *p, const char *curve)
                           "error generate EC key for group: %s", curve); 
             rv = APR_EGENERAL; goto leave;
         }
+#else
+        if (APR_SUCCESS != (rv = check_EC_curve(curve_nid, p))) goto leave;
+        if (NULL == (ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL))
+            || EVP_PKEY_keygen_init(ctx) <= 0
+            || EVP_PKEY_CTX_ctrl_str(ctx, "ec_paramgen_curve", curve) <= 0
+            || EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) <= 0) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p,
+                          "error generate EC key for group: %s", curve);
+            rv = APR_EGENERAL; goto leave;
+        }
+#endif
         rv = APR_SUCCESS;
         break;
     }
@@ -987,8 +1009,6 @@ static apr_status_t sha256_digest(md_data_t **pdigest, apr_pool_t *p, const md_d
     unsigned int dlen;
 
     digest = md_data_pmake(EVP_MAX_MD_SIZE, p);
-    if (!digest) goto leave;
-
     ctx = EVP_MD_CTX_create();
     if (ctx) {
         rv = APR_ENOTIMPL;
@@ -1002,7 +1022,6 @@ static apr_status_t sha256_digest(md_data_t **pdigest, apr_pool_t *p, const md_d
             }
         }
     }
-leave:
     if (ctx) {
         EVP_MD_CTX_destroy(ctx);
     }
@@ -1038,6 +1057,31 @@ apr_status_t md_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p,
     return rv;
 }
 
+apr_status_t md_crypt_hmac64(const char **pmac64, const md_data_t *hmac_key,
+                             apr_pool_t *p, const char *d, size_t dlen)
+{
+    const char *mac64 = NULL;
+    unsigned char *s;
+    unsigned int digest_len = 0;
+    md_data_t *digest;
+    apr_status_t rv = APR_SUCCESS;
+
+    digest = md_data_pmake(EVP_MAX_MD_SIZE, p);
+    s = HMAC(EVP_sha256(), (const unsigned char*)hmac_key->data, (int)hmac_key->len,
+             (const unsigned char*)d, (size_t)dlen,
+             (unsigned char*)digest->data, &digest_len);
+    if (!s) {
+        rv = APR_EINVAL;
+        goto cleanup;
+    }
+    digest->len = digest_len;
+    mac64 = md_util_base64url_encode(digest, p);
+
+cleanup:
+    *pmac64 = (APR_SUCCESS == rv)? mac64 : NULL;
+    return rv;
+}
+
 /**************************************************************************************************/
 /* certificates */
 
@@ -1326,17 +1370,13 @@ apr_status_t md_cert_to_sha256_digest(md_data_t **pdigest, const md_cert_t *cert
 {
     md_data_t *digest;
     unsigned int dlen;
-    apr_status_t rv = APR_ENOMEM;
 
     digest = md_data_pmake(EVP_MAX_MD_SIZE, p);
-    if (!digest) goto leave;
-
     X509_digest(cert->x509, EVP_sha256(), (unsigned char*)digest->data, &dlen);
     digest->len = dlen;
-    rv = APR_SUCCESS;
-leave:
-    *pdigest = (APR_SUCCESS == rv)? digest : NULL;
-    return rv;
+
+    *pdigest = digest;
+    return APR_SUCCESS;
 }
 
 apr_status_t md_cert_to_sha256_fingerprint(const char **pfinger, const md_cert_t *cert, apr_pool_t *p)
@@ -1458,17 +1498,30 @@ apr_status_t md_cert_chain_read_http(struct apr_array_header_t *chain,
     ct = apr_table_get(res->headers, "Content-Type");
     if (!res->body || !ct) goto cleanup;
     ct = md_util_parse_ct(res->req->pool, ct);
-    if (!strcmp("application/pem-certificate-chain", ct)
+    if (!strcmp("application/pkix-cert", ct)) {
+        rv = md_cert_read_http(&cert, p, res);
+        if (APR_SUCCESS != rv) goto cleanup;
+        APR_ARRAY_PUSH(chain, md_cert_t *) = cert;
+    }
+    else if (!strcmp("application/pem-certificate-chain", ct)
         || !strncmp("text/plain", ct, sizeof("text/plain")-1)) {
         /* Some servers seem to think 'text/plain' is sufficient, see #232 */
         rv = apr_brigade_pflatten(res->body, &data, &data_len, res->req->pool);
         if (APR_SUCCESS != rv) goto cleanup;
         rv = md_cert_read_chain(chain, res->req->pool, data, data_len);
     }
-    else if (!strcmp("application/pkix-cert", ct)) {
-        rv = md_cert_read_http(&cert, p, res);
+    else {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p,
+            "attempting to parse certificates from unrecognized content-type: %s", ct);
+        rv = apr_brigade_pflatten(res->body, &data, &data_len, res->req->pool);
         if (APR_SUCCESS != rv) goto cleanup;
-        APR_ARRAY_PUSH(chain, md_cert_t *) = cert;
+        rv = md_cert_read_chain(chain, res->req->pool, data, data_len);
+        if (APR_SUCCESS == rv && chain->nelts == 0) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p,
+                "certificiate chain response did not contain any certificates "
+                "(suspicious content-type: %s)", ct);
+            rv = APR_ENOENT;
+        }
     }
 cleanup:
     md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p,
index 85e45e4e449d35bec56a611f50b0bdce844ad4f9..203dc40f4098068dd9207fa07431fd177dfcdd0a 100644 (file)
@@ -114,6 +114,9 @@ apr_status_t md_crypt_sign64(const char **psign64, md_pkey_t *pkey, apr_pool_t *
 
 void *md_pkey_get_EVP_PKEY(struct md_pkey_t *pkey);
 
+apr_status_t md_crypt_hmac64(const char **pmac64, const struct md_data_t *hmac_key,
+                             apr_pool_t *p, const char *d, size_t dlen);
+
 /**************************************************************************************************/
 /* X509 certificates */
 
index 4bdd99a125db246988b9c1a10898e97611abf895..c0e8c1b4fc4cdc2e9df9aa8732e4766ea7fcab83 100644 (file)
 #include "md_log.h"
 #include "md_util.h"
 
-static int header_set(void *data, const char *key, const char *val)
+apr_status_t md_jws_get_jwk(md_json_t **pjwk, apr_pool_t *p, struct md_pkey_t *pkey)
 {
-    md_json_sets(val, (md_json_t *)data, key, NULL);
-    return 1;
+    md_json_t *jwk;
+
+    if (!pkey) return APR_EINVAL;
+
+    jwk = md_json_create(p);
+    md_json_sets(md_pkey_get_rsa_e64(pkey, p), jwk, "e", NULL);
+    md_json_sets("RSA", jwk, "kty", NULL);
+    md_json_sets(md_pkey_get_rsa_n64(pkey, p), jwk, "n", NULL);
+    *pjwk = jwk;
+    return APR_SUCCESS;
 }
 
 apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p,
-                         md_data_t *payload, struct apr_table_t *protected, 
+                         md_data_t *payload, md_json_t *prot_fields,
                          struct md_pkey_t *pkey, const char *key_id)
 {
-    md_json_t *msg, *jprotected;
+    md_json_t *msg, *jprotected, *jwk;
     const char *prot64, *pay64, *sign64, *sign, *prot;
-    apr_status_t rv = APR_SUCCESS;
+    md_data_t data;
+    apr_status_t rv;
 
-    *pmsg = NULL;
-    
     msg = md_json_create(p);
-
-    jprotected = md_json_create(p);
+    jprotected = md_json_clone(p, prot_fields);
     md_json_sets("RS256", jprotected, "alg", NULL);
     if (key_id) {
         md_json_sets(key_id, jprotected, "kid", NULL);
     }
     else {
-        md_json_sets(md_pkey_get_rsa_e64(pkey, p), jprotected, "jwk", "e", NULL);
-        md_json_sets("RSA", jprotected, "jwk", "kty", NULL);
-        md_json_sets(md_pkey_get_rsa_n64(pkey, p), jprotected, "jwk", "n", NULL);
+        rv = md_jws_get_jwk(&jwk, p, pkey);
+        if (APR_SUCCESS != rv) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "get jwk");
+            goto cleanup;
+        }
+        md_json_setj(jwk, jprotected, "jwk", NULL);
     }
-    apr_table_do(header_set, jprotected, protected, NULL);
-    prot = md_json_writep(jprotected, p, MD_JSON_FMT_COMPACT);
-    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, p, "protected: %s",
-                  prot ? prot : "<failed to serialize!>");
 
+    prot = md_json_writep(jprotected, p, MD_JSON_FMT_COMPACT);
     if (!prot) {
         rv = APR_EINVAL;
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "serialize protected");
+        goto cleanup;
     }
-    
-    if (rv == APR_SUCCESS) {
-        md_data_t data;
 
-        md_data_init(&data, prot, strlen(prot));
-        prot64 = md_util_base64url_encode(&data, p);
-        md_json_sets(prot64, msg, "protected", NULL);
-        pay64 = md_util_base64url_encode(payload, p);
+    md_data_init(&data, prot, strlen(prot));
+    prot64 = md_util_base64url_encode(&data, p);
+    md_json_sets(prot64, msg, "protected", NULL);
 
-        md_json_sets(pay64, msg, "payload", NULL);
-        sign = apr_psprintf(p, "%s.%s", prot64, pay64);
+    pay64 = md_util_base64url_encode(payload, p);
+    md_json_sets(pay64, msg, "payload", NULL);
+    sign = apr_psprintf(p, "%s.%s", prot64, pay64);
 
-        rv = md_crypt_sign64(&sign64, pkey, p, sign, strlen(sign));
+    rv = md_crypt_sign64(&sign64, pkey, p, sign, strlen(sign));
+    if (APR_SUCCESS != rv) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "jwk signed message");
+        goto cleanup;
     }
+    md_json_sets(sign64, msg, "signature", NULL);
 
-    if (rv == APR_SUCCESS) {
-        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, 
-                      "jws pay64=%s\nprot64=%s\nsign64=%s", pay64, prot64, sign64);
-        
-        md_json_sets(sign64, msg, "signature", NULL);
-    }
-    else {
-        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "jwk signed message");
-    } 
-    
+cleanup:
     *pmsg = (APR_SUCCESS == rv)? msg : NULL;
     return rv;
 }
@@ -108,3 +108,41 @@ apr_status_t md_jws_pkey_thumb(const char **pthumb, apr_pool_t *p, struct md_pke
     rv = md_crypt_sha256_digest64(pthumb, p, &data);
     return rv;
 }
+
+apr_status_t md_jws_hmac(md_json_t **pmsg, apr_pool_t *p,
+                         md_data_t *payload, md_json_t *prot_fields,
+                         const md_data_t *hmac_key)
+{
+    md_json_t *msg, *jprotected;
+    const char *prot64, *pay64, *mac64, *sign, *prot;
+    md_data_t data;
+    apr_status_t rv;
+
+    msg = md_json_create(p);
+    jprotected = md_json_clone(p, prot_fields);
+    md_json_sets("HS256", jprotected, "alg", NULL);
+    prot = md_json_writep(jprotected, p, MD_JSON_FMT_COMPACT);
+    if (!prot) {
+        rv = APR_EINVAL;
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "serialize protected");
+        goto cleanup;
+    }
+
+    md_data_init(&data, prot, strlen(prot));
+    prot64 = md_util_base64url_encode(&data, p);
+    md_json_sets(prot64, msg, "protected", NULL);
+
+    pay64 = md_util_base64url_encode(payload, p);
+    md_json_sets(pay64, msg, "payload", NULL);
+    sign = apr_psprintf(p, "%s.%s", prot64, pay64);
+
+    rv = md_crypt_hmac64(&mac64, hmac_key, p, sign, strlen(sign));
+    if (APR_SUCCESS != rv) {
+        goto cleanup;
+    }
+    md_json_sets(mac64, msg, "signature", NULL);
+
+cleanup:
+    *pmsg = (APR_SUCCESS == rv)? msg : NULL;
+    return rv;
+}
index 7121308aaed952ba47c4ed06d2f71af619ce8b2b..466f2dfeabf95c872e396fda1be426524544d7cf 100644 (file)
@@ -22,10 +22,31 @@ struct md_json_t;
 struct md_pkey_t;
 struct md_data_t;
 
+/**
+ * Get the JSON value of the 'jwk' field for the given key.
+ */
+apr_status_t md_jws_get_jwk(md_json_t **pjwk, apr_pool_t *p, struct md_pkey_t *pkey);
+
+/**
+ * Get the JWS key signed JSON message with given payload and protected fields, signed
+ * using the given key and optional key_id.
+ */
 apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p,
-                         struct md_data_t *payload, struct apr_table_t *protected, 
+                         struct md_data_t *payload, md_json_t *prot_fields,
                          struct md_pkey_t *pkey, const char *key_id);
+/**
+ * Get the 'Thumbprint' as defined in RFC8555 for the given key in
+ * base64 encoding.
+ */
+apr_status_t md_jws_pkey_thumb(const char **pthumb64, apr_pool_t *p, struct md_pkey_t *pkey);
+
+/**
+ * Get the JWS HS256 signed message for given payload and protected fields,
+ * using the base64 encoded MAC key.
+ */
+apr_status_t md_jws_hmac(md_json_t **pmsg, apr_pool_t *p,
+                         struct md_data_t *payload, md_json_t *prot_fields,
+                         const struct md_data_t *hmac_key);
 
-apr_status_t md_jws_pkey_thumb(const char **pthumb, apr_pool_t *p, struct md_pkey_t *pkey);
 
 #endif /* md_jws_h */
index b500ddc601a2de4a92f1f02699733ade430ff8e4..67746033eae995f29bef811ece282e5f3e4722a9 100644 (file)
@@ -92,7 +92,8 @@ apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *p, struct md_store_t *st
     reg->can_http = 1;
     reg->can_https = 1;
     reg->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL;
-    reg->ca_file = ca_file? apr_pstrdup(p, ca_file) : NULL;
+    reg->ca_file = (ca_file && apr_strnatcasecmp("none", ca_file))?
+                    apr_pstrdup(p, ca_file) : NULL;
 
     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); 
@@ -197,9 +198,11 @@ static apr_status_t check_values(md_reg_t *reg, apr_pool_t *p, const md_t *md, i
 
 static apr_status_t state_init(md_reg_t *reg, apr_pool_t *p, md_t *md)
 {
-    md_state_t state;
+    md_state_t state = MD_S_COMPLETE;
+    const char *state_descr = NULL;
     const md_pubcert_t *pub;
     const md_cert_t *cert;
+    const md_pkey_spec_t *spec;
     apr_status_t rv = APR_SUCCESS;
     int i;
 
@@ -207,45 +210,50 @@ static apr_status_t state_init(md_reg_t *reg, apr_pool_t *p, md_t *md)
     if (md->warn_window == NULL) md->warn_window = reg->warn_window;
     
     for (i = 0; i < md_cert_count(md); ++i) {
-        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p, "md{%s}: check cert %d", md->name, i);
-        if (APR_SUCCESS == (rv = md_reg_get_pubcert(&pub, reg, md, i, p))) {
+        spec = md_pkeys_spec_get(md->pks, i);
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p,
+                      "md{%s}: check cert %s", md->name, md_pkey_spec_name(spec));
+        rv = md_reg_get_pubcert(&pub, reg, md, i, p);
+        if (APR_SUCCESS == rv) {
             cert = APR_ARRAY_IDX(pub->certs, 0, const md_cert_t*);
             if (!md_is_covered_by_alt_names(md, pub->alt_names)) {
                 state = MD_S_INCOMPLETE;
-                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
-                              "md{%s}: incomplete, certificate(%d) does not cover all domains.",
-                              md->name, i);
-                goto out;
+                state_descr = apr_psprintf(p, "certificate(%s) does not cover all domains.",
+                                           md_pkey_spec_name(spec));
+                goto cleanup;
             }
             if (!md->must_staple != !md_cert_must_staple(cert)) {
                 state = MD_S_INCOMPLETE;
-                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
-                              "md{%s}: incomplete, OCSP Stapling is%s requested, but "
-                              "certificate(%d) has it%s enabled.", 
-                              md->name, md->must_staple? "" : " not", i, 
+                state_descr = apr_psprintf(p, "'must-staple' is%s requested, but "
+                              "certificate(%s) has it%s enabled.",
+                              md->must_staple? "" : " not",
+                              md_pkey_spec_name(spec),
                               !md->must_staple? "" : " not");
-                goto out;
+                goto cleanup;
             }
-            state = MD_S_COMPLETE;
-            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "md{%s}: certificate(%d) is ok", 
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "md{%s}: certificate(%d) is ok",
                           md->name, i);
         }
         else if (APR_STATUS_IS_ENOENT(rv)) {
             state = MD_S_INCOMPLETE;
+            state_descr = apr_psprintf(p, "certificate(%s) is missing",
+                                       md_pkey_spec_name(spec));
             rv = APR_SUCCESS;
-            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
-                          "md{%s}: incomplete, certificate(%d) is missing", md->name, i);
-            goto out;
+            goto cleanup;
+        }
+        else {
+            state = MD_S_ERROR;
+            state_descr = "error intializing";
+            md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "md{%s}: error", md->name);
+            goto cleanup;
         }
     }
 
-out:    
-    if (APR_SUCCESS != rv) {
-        state = MD_S_ERROR;
-        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "md{%s}: error", md->name);
-    }
-    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p, "md{%s}: state==%d", md->name, state);
+cleanup:
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, p, "md{%s}: state=%d, %s",
+                  md->name, state, state_descr);
     md->state = state;
+    md->state_descr = state_descr;
     return rv;
 }
 
index ecc35c66d094445bbf9d03f19ed1d6bbeb371d33..32efc19a67b8ba1306403084327484ac538be6bf 100644 (file)
@@ -88,55 +88,88 @@ leave:
     return rv;
 }
 
-/**************************************************************************************************/
-/* md status information */
-
-static apr_status_t job_loadj(md_json_t **pjson, md_store_group_t group, const char *name, 
+static apr_status_t job_loadj(md_json_t **pjson, md_store_group_t group, const char *name,
                               struct md_reg_t *reg, int with_log, apr_pool_t *p)
 {
     apr_status_t rv;
-    
+
     md_store_t *store = md_reg_store_get(reg);
     rv = md_store_load_json(store, group, name, MD_FN_JOB, pjson, p);
     if (APR_SUCCESS == rv && !with_log) md_json_del(*pjson, MD_KEY_LOG, NULL);
     return rv;
 }
 
+static apr_status_t status_get_cert_json_ex(
+    md_json_t **pjson,
+    const md_cert_t *cert,
+    const md_t *md,
+    md_reg_t *reg,
+    md_ocsp_reg_t *ocsp,
+    int with_logs,
+    apr_pool_t *p)
+{
+    md_json_t *certj, *jobj;
+    md_timeperiod_t ocsp_valid;
+    md_ocsp_cert_stat_t cert_stat;
+    apr_status_t rv;
+
+    if (APR_SUCCESS != (rv = status_get_cert_json(&certj, cert, p))) goto leave;
+    if (md->stapling && ocsp) {
+        rv = md_ocsp_get_meta(&cert_stat, &ocsp_valid, ocsp, cert, p, md);
+        if (APR_SUCCESS == rv) {
+            md_json_sets(md_ocsp_cert_stat_name(cert_stat), certj, MD_KEY_OCSP, MD_KEY_STATUS, NULL);
+            md_json_set_timeperiod(&ocsp_valid, certj, MD_KEY_OCSP, MD_KEY_VALID, NULL);
+        }
+        else if (!APR_STATUS_IS_ENOENT(rv)) goto leave;
+        rv = APR_SUCCESS;
+        if (APR_SUCCESS == job_loadj(&jobj, MD_SG_OCSP, md->name, reg, with_logs, p)) {
+            md_json_setj(jobj, certj, MD_KEY_OCSP, MD_KEY_RENEWAL, NULL);
+        }
+    }
+leave:
+    *pjson = (APR_SUCCESS == rv)? certj : NULL;
+    return rv;
+}
+
+static int get_cert_count(const md_t *md, int from_staging)
+{
+    if (!from_staging && md->cert_files && md->cert_files->nelts) {
+        return md->cert_files->nelts;
+    }
+    return md_pkeys_spec_count(md->pks);
+}
+
+static const char *get_cert_name(const md_t *md, int i, int from_staging, apr_pool_t *p)
+{
+    if (!from_staging && md->cert_files && md->cert_files->nelts) {
+        /* static files configured, not from staging, used index names */
+        return apr_psprintf(p, "%d", i);
+    }
+    return md_pkey_spec_name(md_pkeys_spec_get(md->pks, i));
+}
+
 static apr_status_t status_get_certs_json(md_json_t **pjson, apr_array_header_t *certs,
+                                          int from_staging,
                                           const md_t *md, md_reg_t *reg,  
-                                          md_ocsp_reg_t *ocsp, int with_logs, 
+                                          md_ocsp_reg_t *ocsp, int with_logs,
                                           apr_pool_t *p)
 {
-    md_json_t *json, *certj, *jobj;
-    md_timeperiod_t certs_valid = {0, 0}, valid, ocsp_valid;
-    md_pkey_spec_t *spec;
+    md_json_t *json, *certj;
+    md_timeperiod_t certs_valid = {0, 0}, valid;
     md_cert_t *cert;
-    md_ocsp_cert_stat_t cert_stat;
     int i;
     apr_status_t rv = APR_SUCCESS;   
     
     json = md_json_create(p);
-    for (i = 0; i < md_cert_count(md); ++i) {
-        spec = md_pkeys_spec_get(md->pks, i);
+    for (i = 0; i < get_cert_count(md, from_staging); ++i) {
         cert = APR_ARRAY_IDX(certs, i, md_cert_t*);
         if (!cert) continue;
-        
-        if (APR_SUCCESS != (rv = status_get_cert_json(&certj, cert, p))) goto leave;
-        if (md->stapling && ocsp) {
-            rv = md_ocsp_get_meta(&cert_stat, &ocsp_valid, ocsp, cert, p, md);
-            if (APR_SUCCESS == rv) {
-                md_json_sets(md_ocsp_cert_stat_name(cert_stat), certj, MD_KEY_OCSP, MD_KEY_STATUS, NULL);
-                md_json_set_timeperiod(&ocsp_valid, certj, MD_KEY_OCSP, MD_KEY_VALID, NULL);
-            }
-            else if (!APR_STATUS_IS_ENOENT(rv)) goto leave;
-            rv = APR_SUCCESS;
-            if (APR_SUCCESS == job_loadj(&jobj, MD_SG_OCSP, md->name, reg, with_logs, p)) {
-                md_json_setj(jobj, certj, MD_KEY_OCSP, MD_KEY_RENEWAL, NULL);
-            }
-        }
+
+        rv = status_get_cert_json_ex(&certj, cert, md, reg, ocsp, with_logs, p);
+        if (APR_SUCCESS != rv) goto leave;
         valid = md_cert_get_valid(cert);
         certs_valid = i? md_timeperiod_common(&certs_valid, &valid) : valid;
-        md_json_setj(certj, json, md_pkey_spec_name(spec), NULL);
+        md_json_setj(certj, json, get_cert_name(md, i, from_staging, p), NULL);
     }
     
     if (certs_valid.start) {
@@ -157,7 +190,7 @@ static apr_status_t get_staging_certs_json(md_json_t **pjson, const md_t *md,
     apr_status_t rv;
     
     certs = apr_array_make(p, 5, sizeof(md_cert_t*));
-    for (i = 0; i < md_cert_count(md); ++i) {
+    for (i = 0; i < get_cert_count(md, 1); ++i) {
         spec = md_pkeys_spec_get(md->pks, i);
         cert = NULL;
         rv = md_pubcert_load(md_reg_store_get(reg), MD_SG_STAGING, md->name, spec, &chain, p);
@@ -166,7 +199,7 @@ static apr_status_t get_staging_certs_json(md_json_t **pjson, const md_t *md,
         }
         APR_ARRAY_PUSH(certs, const md_cert_t*) = cert;
     }
-    return status_get_certs_json(pjson, certs, md, reg, NULL, 0, p);
+    return status_get_certs_json(pjson, certs, 1, md, reg, NULL, 0, p);
 }
 
 static apr_status_t status_get_md_json(md_json_t **pjson, const md_t *md, 
@@ -182,9 +215,9 @@ static apr_status_t status_get_md_json(md_json_t **pjson, const md_t *md,
     apr_time_t renew_at;
     int i;
 
-    mdj = md_to_json(md, p);
+    mdj = md_to_public_json(md, p);
     certs = apr_array_make(p, 5, sizeof(md_cert_t*));
-    for (i = 0; i < md_cert_count(md); ++i) {
+    for (i = 0; i < get_cert_count(md, 0); ++i) {
         cert = NULL;
         if (APR_SUCCESS == md_reg_get_pubcert(&pubcert, reg, md, i, p)) {
             cert = APR_ARRAY_IDX(pubcert->certs, 0, const md_cert_t*);
@@ -192,7 +225,7 @@ static apr_status_t status_get_md_json(md_json_t **pjson, const md_t *md,
         APR_ARRAY_PUSH(certs, const md_cert_t*) = cert;
     }
     
-    rv = status_get_certs_json(&certsj, certs, md, reg, ocsp, with_logs, p);
+    rv = status_get_certs_json(&certsj, certs, 0, md, reg, ocsp, with_logs, p);
     if (APR_SUCCESS != rv) goto leave;
     md_json_setj(certsj, mdj, MD_KEY_CERT, NULL);
     
@@ -605,8 +638,9 @@ apr_status_t md_job_notify(md_job_t *job, const char *reason, md_result_t *resul
     job->dirty = 1;
     if (APR_SUCCESS == rv && APR_SUCCESS == result->status) {
         job->notified = 1;
-        if (!strcmp("renewed", reason)) job->notified_renewed = 1;
-        job->error_runs = 0;
+        if (!strcmp("renewed", reason)) {
+            job->notified_renewed = 1;
+        }
     }
     else {
         ++job->error_runs;
index 959bafee80f587b94752d5476a40112613172b3b..f70542cbb7de6acee356f2309eb5b0b605286c64 100644 (file)
@@ -631,7 +631,7 @@ static apr_status_t pfs_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l
         && MD_OK(mk_group_dir(&dir, s_fs, group, name, p))
         && MD_OK(md_util_path_merge(&fpath, ptemp, dir, aspect, NULL))) {
         
-        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "storing in %s", fpath);
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, ptemp, "storing in %s", fpath);
         switch (vtype) {
             case MD_SV_TEXT:
                 rv = (create? md_text_fcreatex(fpath, perms->file, p, value)
@@ -745,7 +745,9 @@ static apr_status_t pfs_purge(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_
         /* Remove all files in dir, there should be no sub-dirs */
         rv = md_util_rm_recursive(dir, ptemp, 1);
     }
-    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, ptemp, "purge %s/%s (%s)", groupname, name, dir);
+    if (!APR_STATUS_IS_ENOENT(rv)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, ptemp, "purge %s/%s (%s)", groupname, name, dir);
+    }
     return APR_SUCCESS;
 }
 
@@ -1018,12 +1020,12 @@ static apr_status_t pfs_move(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l
         }
         
         if (!MD_OK(apr_file_rename(to_dir, narch_dir, ptemp))) {
-                md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", 
+                md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s",
                               to_dir, narch_dir);
                 goto out;
         }
         if (!MD_OK(apr_file_rename(from_dir, to_dir, ptemp))) {
-            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", 
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s",
                           from_dir, to_dir);
             apr_file_rename(narch_dir, to_dir, ptemp);
             goto out;
@@ -1034,7 +1036,7 @@ static apr_status_t pfs_move(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_l
     }
     else if (APR_STATUS_IS_ENOENT(rv)) {
         if (APR_SUCCESS != (rv = apr_file_rename(from_dir, to_dir, ptemp))) {
-            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", 
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s",
                           from_dir, to_dir);
             goto out;
         }
@@ -1075,8 +1077,9 @@ static apr_status_t pfs_rename(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va
         goto out;
     }
     
-    if (APR_SUCCESS != (rv = apr_file_rename(from_dir, to_dir, ptemp))) {
-        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", 
+    if (APR_SUCCESS != (rv = apr_file_rename(from_dir, to_dir, ptemp))
+        && !APR_STATUS_IS_ENOENT(rv)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s",
                       from_dir, to_dir);
         goto out;
     }
index 65aa56a462d0aac3a3be336b855d5b31140bee4c..ae723f621ff2342518f7447135f63da16e52d55e 100644 (file)
@@ -27,7 +27,7 @@
  * @macro
  * Version number of the md module as c string
  */
-#define MOD_MD_VERSION "2.4.7"
+#define MOD_MD_VERSION "2.4.10"
 
 /**
  * @macro
@@ -35,7 +35,7 @@
  * release. This is a 24 bit number with 8 bits for major number, 8 bits
  * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203.
  */
-#define MOD_MD_VERSION_NUM 0x020407
+#define MOD_MD_VERSION_NUM 0x02040a
 
 #define MD_ACME_DEF_URL    "https://acme-v02.api.letsencrypt.org/directory"
 
index 8b379eb1751fc625eff5ea264f02face92e1bd36..6387e4dd2007be8ff7cf90316d4b1e1e1e668628 100644 (file)
@@ -54,7 +54,6 @@
 #include "mod_md_ocsp.h"
 #include "mod_md_os.h"
 #include "mod_md_status.h"
-#include "mod_ssl_openssl.h"
 
 static void md_hooks(apr_pool_t *pool);
 
@@ -324,11 +323,15 @@ static void merge_srv_config(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p)
         md->ca_agreement = md_config_gets(md->sc, MD_CONFIG_CA_AGREEMENT);
     }
     contact = md_config_gets(md->sc, MD_CONFIG_CA_CONTACT);
-    if (contact && contact[0]) {
+    if (md->contacts && md->contacts->nelts > 0) {
+        /* set explicitly */
+    }
+    else if (contact && contact[0]) {
         apr_array_clear(md->contacts);
         APR_ARRAY_PUSH(md->contacts, const char *) =
         md_util_schemify(p, 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");
@@ -350,6 +353,10 @@ static void merge_srv_config(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p)
     if (md->require_https < 0) {
         md->require_https = md_config_geti(md->sc, MD_CONFIG_REQUIRE_HTTPS);
     }
+    if (!md->ca_eab_kid) {
+        md->ca_eab_kid = md->sc->ca_eab_kid;
+        md->ca_eab_hmac = md->sc->ca_eab_hmac;
+    }
     if (md->must_staple < 0) {
         md->must_staple = md_config_geti(md->sc, MD_CONFIG_MUST_STAPLE);
     }
@@ -592,14 +599,18 @@ static apr_status_t link_md_to_servers(md_mod_conf_t *mc, md_t *md, server_rec *
                              s->server_hostname, s->port, md->name, sc->name,
                              domain, (int)sc->assigned->nelts);
 
-                if (sc->ca_contact && sc->ca_contact[0]) {
+                if (md->contacts && md->contacts->nelts > 0) {
+                    /* set explicitly */
+                }
+                else if (sc->ca_contact && sc->ca_contact[0]) {
                     uri = md_util_schemify(p, sc->ca_contact, "mailto");
                     if (md_array_str_index(md->contacts, uri, 0, 0) < 0) {
                         APR_ARRAY_PUSH(md->contacts, const char *) = uri;
                         ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10044)
                                      "%s: added contact %s", md->name, uri);
                     }
-                } else if (s->server_admin && strcmp(DEFAULT_ADMIN, s->server_admin)) {
+                }
+                else if (s->server_admin && strcmp(DEFAULT_ADMIN, s->server_admin)) {
                     uri = md_util_schemify(p, s->server_admin, "mailto");
                     if (md_array_str_index(md->contacts, uri, 0, 0) < 0) {
                         APR_ARRAY_PUSH(md->contacts, const char *) = uri;
@@ -675,18 +686,18 @@ static apr_status_t merge_mds_with_conf(md_mod_conf_t *mc, apr_pool_t *p,
         if (md->cert_files && md->cert_files->nelts) {
             if (!md->pkey_files || (md->cert_files->nelts != md->pkey_files->nelts)) {
                 ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10170)
-                             "The Managed Domain '%s', defined in %s(line %d), "
+                             "The Managed Domain '%s' "
                              "needs one MDCertificateKeyFile for each MDCertificateFile.",
-                             md->name, md->defn_name, md->defn_line_number);
+                             md->name);
                 return APR_EINVAL;
             }
         }
         else if (md->pkey_files && md->pkey_files->nelts 
             && (!md->cert_files || !md->cert_files->nelts)) {
             ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10171)
-                         "The Managed Domain '%s', defined in %s(line %d), "
+                         "The Managed Domain '%s' "
                          "has MDCertificateKeyFile(s) but no MDCertificateFile.",
-                         md->name, md->defn_name, md->defn_line_number);
+                         md->name);
             return APR_EINVAL;
         }
 
index bfc64adbbd60a794ca060ddb386acc8b9aa3566f..8d3260634ecb72bd6eeb95b114151902caa0d0c6 100644 (file)
@@ -28,6 +28,7 @@
 #include "md.h"
 #include "md_crypt.h"
 #include "md_log.h"
+#include "md_json.h"
 #include "md_util.h"
 #include "mod_md_private.h"
 #include "mod_md_config.h"
@@ -110,6 +111,8 @@ static md_srv_conf_t defconf = {
     "ACME",                    /* ca protocol */
     NULL,                      /* ca agreemnent */
     NULL,                      /* ca challenges array */
+    NULL,                      /* ca eab kid */
+    NULL,                      /* ca eab hmac */
     0,                         /* stapling */
     1,                         /* staple others */
     NULL,                      /* currently defined md */
@@ -162,6 +165,8 @@ static void srv_conf_props_clear(md_srv_conf_t *sc)
     sc->ca_proto = NULL;
     sc->ca_agreement = NULL;
     sc->ca_challenges = NULL;
+    sc->ca_eab_kid = NULL;
+    sc->ca_eab_hmac = NULL;
     sc->stapling = DEF_VAL;
     sc->staple_others = DEF_VAL;
 }
@@ -180,6 +185,8 @@ static void srv_conf_props_copy(md_srv_conf_t *to, const md_srv_conf_t *from)
     to->ca_proto = from->ca_proto;
     to->ca_agreement = from->ca_agreement;
     to->ca_challenges = from->ca_challenges;
+    to->ca_eab_kid = from->ca_eab_kid;
+    to->ca_eab_hmac = from->ca_eab_hmac;
     to->stapling = from->stapling;
     to->staple_others = from->staple_others;
 }
@@ -196,7 +203,14 @@ static void srv_conf_props_apply(md_t *md, const md_srv_conf_t *from, apr_pool_t
     if (from->ca_url) md->ca_url = from->ca_url;
     if (from->ca_proto) md->ca_proto = from->ca_proto;
     if (from->ca_agreement) md->ca_agreement = from->ca_agreement;
+    if (from->ca_contact) {
+        apr_array_clear(md->contacts);
+        APR_ARRAY_PUSH(md->contacts, const char *) =
+            md_util_schemify(p, from->ca_contact, "mailto");
+    }
     if (from->ca_challenges) md->ca_challenges = apr_array_copy(p, from->ca_challenges);
+    if (from->ca_eab_kid) md->ca_eab_kid = from->ca_eab_kid;
+    if (from->ca_eab_hmac) md->ca_eab_hmac = from->ca_eab_hmac;
     if (from->stapling != DEF_VAL) md->stapling = from->stapling;
 }
 
@@ -238,6 +252,8 @@ static void *md_config_merge(apr_pool_t *pool, void *basev, void *addv)
     nsc->ca_agreement = add->ca_agreement? add->ca_agreement : base->ca_agreement;
     nsc->ca_challenges = (add->ca_challenges? apr_array_copy(pool, add->ca_challenges) 
                     : (base->ca_challenges? apr_array_copy(pool, base->ca_challenges) : NULL));
+    nsc->ca_eab_kid = add->ca_eab_kid? add->ca_eab_kid : base->ca_eab_kid;
+    nsc->ca_eab_hmac = add->ca_eab_hmac? add->ca_eab_hmac : base->ca_eab_hmac;
     nsc->stapling = (add->stapling != DEF_VAL)? add->stapling : base->stapling;
     nsc->staple_others = (add->staple_others != DEF_VAL)? add->staple_others : base->staple_others;
     nsc->current = NULL;
@@ -461,13 +477,16 @@ static const char *md_config_set_names(cmd_parms *cmd, void *dc,
 static const char *md_config_set_ca(cmd_parms *cmd, void *dc, const char *value)
 {
     md_srv_conf_t *sc = md_config_get(cmd->server);
-    const char *err;
+    const char *err, *url;
 
     (void)dc;
     if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
         return err;
     }
-    sc->ca_url = value;
+    if (APR_SUCCESS != md_get_ca_url_from_name(&url, cmd->pool, value)) {
+        return url;
+    }
+    sc->ca_url = url;
     return NULL;
 }
 
@@ -1009,9 +1028,69 @@ static const char *md_config_set_ca_certs(cmd_parms *cmd, void *dc, const char *
     return NULL;
 }
 
+static const char *md_config_set_eab(cmd_parms *cmd, void *dc,
+                                     const char *keyid, const char *hmac)
+{
+    md_srv_conf_t *sc = md_config_get(cmd->server);
+    const char *err;
+
+    (void)dc;
+    if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) {
+        return err;
+    }
+    if (!hmac) {
+        if (!apr_strnatcasecmp("None", keyid)) {
+            keyid = "none";
+        }
+        else {
+            /* a JSON file keeping keyid and hmac */
+            const char *fpath;
+            apr_status_t rv;
+            md_json_t *json;
+
+            /* If only dumping the config, don't verify the file */
+            if (ap_state_query(AP_SQ_RUN_MODE) == AP_SQ_RM_CONFIG_DUMP) {
+                goto leave;
+            }
+
+            fpath = ap_server_root_relative(cmd->pool, keyid);
+            if (!fpath) {
+                return apr_pstrcat(cmd->pool, cmd->cmd->name,
+                                   ": Invalid file path ", keyid, NULL);
+            }
+            if (!md_file_exists(fpath, cmd->pool)) {
+                return apr_pstrcat(cmd->pool, cmd->cmd->name,
+                                   ": file not found: ", fpath, NULL);
+            }
+
+            rv = md_json_readf(&json, cmd->pool, fpath);
+            if (APR_SUCCESS != rv) {
+                return apr_pstrcat(cmd->pool, cmd->cmd->name,
+                                   ": error reading JSON file ", fpath, NULL);
+            }
+            keyid = md_json_gets(json, MD_KEY_KID, NULL);
+            if (!keyid || !*keyid) {
+                return apr_pstrcat(cmd->pool, cmd->cmd->name,
+                                   ": JSON does not contain '", MD_KEY_KID,
+                                   "' element in file ", fpath, NULL);
+            }
+            hmac = md_json_gets(json, MD_KEY_HMAC, NULL);
+            if (!hmac || !*hmac) {
+                return apr_pstrcat(cmd->pool, cmd->cmd->name,
+                                   ": JSON does not contain '", MD_KEY_HMAC,
+                                   "' element in file ", fpath, NULL);
+            }
+        }
+    }
+leave:
+    sc->ca_eab_kid = keyid;
+    sc->ca_eab_hmac = hmac;
+    return NULL;
+}
+
 const command_rec md_cmds[] = {
     AP_INIT_TAKE1("MDCertificateAuthority", md_config_set_ca, NULL, RSRC_CONF, 
-                  "URL of CA issuing the certificates"),
+                  "URL or known name of CA issuing the certificates"),
     AP_INIT_TAKE1("MDCertificateAgreement", md_config_set_agreement, NULL, RSRC_CONF, 
                   "either 'accepted' or the URL of CA Terms-of-Service agreement you accept"),
     AP_INIT_TAKE_ARGV("MDCAChallenges", md_config_set_cha_tyes, NULL, RSRC_CONF, 
@@ -1085,6 +1164,8 @@ const command_rec md_cmds[] = {
                   "How long to delay activation of new certificates"),
     AP_INIT_TAKE1("MDCACertificateFile", md_config_set_ca_certs, NULL, RSRC_CONF,
                   "Set the CA file to use for connections"),
+    AP_INIT_TAKE12("MDExternalAccountBinding", md_config_set_eab, NULL, RSRC_CONF,
+                  "Set the external account binding keyid and hmac values to use at CA"),
 
     AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL)
 };
index 76423bd602ab4d4a43984e3473efa8dc51af32d6..35c315298105e7c87276a53f0413865f3eaaf411 100644 (file)
@@ -91,7 +91,9 @@ typedef struct md_srv_conf_t {
     const char *ca_proto;              /* protocol used vs CA (e.g. ACME) */
     const char *ca_agreement;          /* accepted agreement uri between CA and user */ 
     struct apr_array_header_t *ca_challenges; /* challenge types configured */
-    
+    const char *ca_eab_kid;            /* != NULL, external account binding keyid */
+    const char *ca_eab_hmac;           /* != NULL, external account binding hmac */
+
     int stapling;                      /* OCSP stapling enabled */
     int staple_others;                 /* Provide OCSP stapling for non-MD certificates */
 
index cb0e2677f06d8534d3d2d2304f034ef18d00a611..390290b85cd48ca3872adca02a4386e2e7410ae8 100644 (file)
@@ -172,7 +172,10 @@ static void si_val_status(status_ctx *ctx, md_json_t *mdj, const status_info *in
     apr_time_t until;
     (void)info;
     switch (md_json_getl(mdj, info->key, NULL)) {
-        case MD_S_INCOMPLETE: s = "incomplete"; break;
+        case MD_S_INCOMPLETE:
+            s = md_json_gets(mdj, MD_KEY_STATE_DESCR, NULL);
+            s = s? apr_psprintf(ctx->p, "incomplete: %s", s) : "incomplete";
+            break;
         case MD_S_EXPIRED_DEPRECATED:
         case MD_S_COMPLETE:
             until = md_json_get_time(mdj, MD_KEY_CERT, MD_KEY_VALID, MD_KEY_UNTIL, NULL);
@@ -188,28 +191,11 @@ static void si_val_status(status_ctx *ctx, md_json_t *mdj, const status_info *in
 static void si_val_url(status_ctx *ctx, md_json_t *mdj, const status_info *info)
 {
     const char *url, *s;
-    apr_uri_t uri_parsed;
 
-    
     s = url = md_json_gets(mdj, info->key, NULL);
     if (!url) return;
-    if (!strcmp(LE_ACMEv2_PROD, url)) {
-        s = "Let's Encrypt";
-    }
-    else if (!strcmp(LE_ACMEv2_STAGING, url)) {
-        s = "Let's Encrypt (staging)";
-    }
-    else if (!strcmp(LE_ACMEv1_PROD, url)) {
-        s = "Let's Encrypt (v1)";
-    }
-    else if (!strcmp(LE_ACMEv1_STAGING, url)) {
-        s = "Let's Encrypt (v1,staging)";
-    }
-    else if (APR_SUCCESS == apr_uri_parse(ctx->p, url, &uri_parsed)) {
-        s = uri_parsed.hostname;
-        
-    }
-    apr_brigade_printf(ctx->bb, NULL, NULL, "<a href='%s'>%s</a>", 
+    s = md_get_ca_name_from_url(ctx->p, url);
+    apr_brigade_printf(ctx->bb, NULL, NULL, "<a href='%s'>%s</a>",
                        ap_escape_html2(ctx->p, url, 1), 
                        ap_escape_html2(ctx->p, s, 1));
 }
@@ -326,13 +312,24 @@ static void si_val_ca_url(status_ctx *ctx, md_json_t *mdj, const status_info *in
     jcert = md_json_getj(mdj, info->key, NULL);
     if (jcert) si_val_url(ctx, jcert, &sub);
 }
-    
+
+static int count_certs(void *baton, const char *key, md_json_t *json)
+{
+    int *pcount = baton;
+
+    (void)json;
+    if (strcmp(key, MD_KEY_VALID)) {
+        *pcount += 1;
+    }
+    return 1;
+}
+
 static void print_job_summary(apr_bucket_brigade *bb, md_json_t *mdj, const char *key, 
                               const char *separator)
 {
     char buffer[HUGE_STRING_LEN];
     apr_status_t rv;
-    int finished, errors;
+    int finished, errors, cert_count;
     apr_time_t t;
     const char *s, *line;
     
@@ -353,8 +350,16 @@ static void print_job_summary(apr_bucket_brigade *bb, md_json_t *mdj, const char
     }
     
     if (finished) {
-        line = apr_psprintf(bb->p, "%s finished successfully.", line);
-    } 
+        cert_count = 0;
+        md_json_iterkey(count_certs, &cert_count, mdj, key, MD_KEY_CERT, NULL);
+        if (cert_count > 0) {
+            line =apr_psprintf(bb->p, "%s  finished, %d new certificate%s staged.",
+                               line, cert_count, cert_count > 1? "s" : "");
+        }
+        else {
+            line = apr_psprintf(bb->p, "%s  finished successfully.", line);
+        }
+    }
     else {
         s = md_json_gets(mdj, key, MD_KEY_LAST, MD_KEY_DETAIL, NULL);
         if (s) line = apr_psprintf(bb->p, "%s %s", line, s);