--- /dev/null
+ *) 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.
+
--- /dev/null
+ *) 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]
+
--- /dev/null
+ *) mod_md: Fix memory leak in case of failures to load the private key.
+ PR 65620 [ Filipe Casal <filipe.casal@trailofbits.com> ]
</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>
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 */
#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"
#define MD_KEY_FINISHED "finished"
#define MD_KEY_FROM "from"
#define MD_KEY_GOOD "good"
+#define MD_KEY_HMAC "hmac"
#define MD_KEY_HTTP "http"
#define MD_KEY_HTTPS "https"
#define MD_KEY_ID "id"
#define MD_KEY_IDENTIFIER "identifier"
#define MD_KEY_KEY "key"
+#define MD_KEY_KID "kid"
#define MD_KEY_KEYAUTHZ "keyAuthorization"
#define MD_KEY_LAST "last"
#define MD_KEY_LAST_RUN "last-run"
#define MD_KEY_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"
*
* 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 */
{ "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 },
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;
switch (res->status) {
case 400:
return APR_EINVAL;
+ case 401: /* sectigo returns this instead of 403 */
case 403:
return APR_EACCES;
case 404:
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)
"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;
}
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 {
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;
&& 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;
} 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;
/**
* 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.
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 */
#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"
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 *));
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;
}
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 *));
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;
}
/**************************************************************************************************/
/* 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;
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;
}
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;
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 */
}
* 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;
}
md_acme_t *acme;
apr_pool_t *p;
const char *agreement;
+ const char *eab_kid;
+ const char *eab_hmac;
} acct_ctx_t;
/**************************************************************************************************/
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) {
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);
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);
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) {
/**************************************************************************************************/
/* 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;
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) {
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);
*/
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;
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,
}
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);
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);
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 */
};
* 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,
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 */
/* 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);
/* 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);
/* 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;
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)) {
/* 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;
/* 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)",
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);
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;
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);
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
* 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;
}
}
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))) {
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;
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:
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,
* 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;
* 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");
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;
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 */
* * 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,
#include <apr_lib.h>
#include <apr_strings.h>
+#include <apr_uri.h>
#include <apr_tables.h>
#include <apr_time.h>
#include <apr_date.h>
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);
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;
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);
}
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;
+}
#include <openssl/err.h>
#include <openssl/evp.h>
+#include <openssl/hmac.h>
#include <openssl/pem.h>
#include <openssl/rand.h>
#include <openssl/rsa.h>
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;
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;
cb_baton = &ctx;
cipher = EVP_aes_256_cbc();
if (!cipher) {
- return APR_ENOTIMPL;
+ rv = APR_ENOTIMPL;
+ goto cleanup;
}
}
#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);
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,
#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) {
#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
"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;
}
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;
}
}
}
-leave:
if (ctx) {
EVP_MD_CTX_destroy(ctx);
}
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 */
{
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)
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,
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 */
#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;
}
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;
+}
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 */
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(®->renew_window, p, MD_TIME_LIFE_NORM, MD_TIME_RENEW_WINDOW_DEF);
md_timeslice_create(®->warn_window, p, MD_TIME_LIFE_NORM, MD_TIME_WARN_WINDOW_DEF);
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;
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;
}
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) {
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);
}
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,
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*);
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);
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;
&& 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)
/* 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;
}
}
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;
}
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;
}
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;
}
* @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
* 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"
#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);
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");
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);
}
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;
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;
}
#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"
"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 */
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;
}
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;
}
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;
}
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;
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;
}
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,
"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)
};
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 */
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);
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));
}
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;
}
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);