diff options
Diffstat (limited to 'modules/md/md_acme_drive.c')
-rw-r--r-- | modules/md/md_acme_drive.c | 1316 |
1 files changed, 699 insertions, 617 deletions
diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c index ba4e865..4bb04f3 100644 --- a/modules/md/md_acme_drive.c +++ b/modules/md/md_acme_drive.c @@ -29,6 +29,7 @@ #include "md_jws.h" #include "md_http.h" #include "md_log.h" +#include "md_result.h" #include "md_reg.h" #include "md_store.h" #include "md_util.h" @@ -36,315 +37,160 @@ #include "md_acme.h" #include "md_acme_acct.h" #include "md_acme_authz.h" +#include "md_acme_order.h" -typedef struct { - md_proto_driver_t *driver; - - const char *phase; - int complete; +#include "md_acme_drive.h" +#include "md_acmev2_drive.h" - md_pkey_t *privkey; /* the new private key */ - apr_array_header_t *pubcert; /* the new certificate + chain certs */ - - md_cert_t *cert; /* the new certificate */ - apr_array_header_t *chain; /* the chain certificates */ - const char *next_up_link; /* where the next chain cert is */ - - md_acme_t *acme; - md_t *md; - const md_creds_t *ncreds; +/**************************************************************************************************/ +/* account setup */ + +static apr_status_t use_staged_acct(md_acme_t *acme, struct md_store_t *store, + const md_t *md, apr_pool_t *p) +{ + md_acme_acct_t *acct; + md_pkey_t *pkey; + apr_status_t rv; - apr_array_header_t *ca_challenges; - md_acme_authz_set_t *authz_set; - apr_interval_time_t authz_monitor_timeout; + if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey, store, + MD_SG_STAGING, md->name, acme->p))) { + acme->acct_id = NULL; + acme->acct = acct; + acme->acct_key = pkey; + rv = md_acme_acct_validate(acme, NULL, p); + } + return rv; +} + +static apr_status_t save_acct_staged(md_acme_t *acme, md_store_t *store, + const char *md_name, apr_pool_t *p) +{ + md_json_t *jacct; + apr_status_t rv; - const char *csr_der_64; - apr_interval_time_t cert_poll_timeout; + jacct = md_acme_acct_to_json(acme->acct, p); -} md_acme_driver_t; - -/**************************************************************************************************/ -/* account setup */ + rv = md_store_save(store, p, MD_SG_STAGING, md_name, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 0); + if (APR_SUCCESS == rv) { + rv = md_store_save(store, p, MD_SG_STAGING, md_name, MD_FN_ACCT_KEY, + MD_SV_PKEY, acme->acct_key, 0); + } + return rv; +} -static apr_status_t ad_set_acct(md_proto_driver_t *d) +apr_status_t md_acme_drive_set_acct(md_proto_driver_t *d, md_result_t *result) { md_acme_driver_t *ad = d->baton; md_t *md = ad->md; apr_status_t rv = APR_SUCCESS; - int update = 0, acct_installed = 0; + int update_md = 0, update_acct = 0; + + md_result_activity_printf(result, "Selecting account to use for %s", d->md->name); + md_acme_clear_acct(ad->acme); - ad->phase = "setup acme"; - if (!ad->acme - && APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, md->ca_url, d->proxy_url))) { - goto out; - } - - ad->phase = "choose account"; /* Do we have a staged (modified) account? */ - if (APR_SUCCESS == (rv = md_acme_use_acct_staged(ad->acme, d->store, md, 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"); - md->ca_account = MD_ACME_ACCT_STAGED; - acct_installed = 1; } - else if (APR_STATUS_IS_ENOENT(rv)) { - rv = APR_SUCCESS; + else if (!APR_STATUS_IS_ENOENT(rv)) { + goto leave; } /* Get an account for the ACME server for this MD */ - if (md->ca_account && !acct_installed) { + 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; - update = 1; - rv = APR_SUCCESS; + update_md = 1; + } + else if (APR_SUCCESS != rv) { + goto leave; } } - if (APR_SUCCESS == rv && !md->ca_account) { + if (!ad->acme->acct && !md->ca_account) { /* 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 == md_acme_find_acct(ad->acme, d->store, d->p)) { - md->ca_account = md_acme_get_acct_id(ad->acme); - update = 1; + 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)", + d->proto->protocol, ad->acme->acct->url, md->ca_account); } } - if (APR_SUCCESS == rv && !md->ca_account) { - /* 2.2 No local account exists, create a new one */ + if (!ad->acme->acct) { + /* No account staged, no suitable found in store, register a new one */ + md_result_activity_printf(result, "Creating new ACME account for %s", d->md->name); md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: creating new account", d->proto->protocol); if (!ad->md->contacts || apr_is_empty_array(md->contacts)) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, - "no contact information for md %s", md->name); rv = APR_EINVAL; - goto out; + md_result_printf(result, rv, "No contact information is available for MD %s. " + "Configure one using the MDContactEmail or ServerAdmin directive.", md->name); + md_result_log(result, MD_LOG_ERR); + goto leave; } - - if (APR_SUCCESS == (rv = md_acme_create_acct(ad->acme, d->p, md->contacts, - md->ca_agreement)) - && APR_SUCCESS == (rv = md_acme_acct_save_staged(ad->acme, d->store, md, d->p))) { - md->ca_account = MD_ACME_ACCT_STAGED; - update = 1; + + /* ACMEv1 allowed registration of accounts without accepted Terms-of-Service. + * ACMEv2 requires it. Fail early in this case with a meaningful error message. + */ + if (!md->ca_agreement) { + md_result_printf(result, APR_EINVAL, + "the CA requires you to accept the terms-of-service " + "as specified in <%s>. " + "Please read the document that you find at that URL and, " + "if you agree to the conditions, configure " + "\"MDCertificateAgreement accepted\" " + "in your Apache. Then (graceful) restart the server to activate.", + ad->acme->ca_agreement); + md_result_log(result, MD_LOG_ERR); + rv = result->status; + goto leave; } - } -out: - if (APR_SUCCESS == rv) { - const char *agreement = md_acme_get_agreement(ad->acme); - /* Persist the account chosen at the md so we use the same on future runs */ - if (agreement && !md->ca_agreement) { - md->ca_agreement = agreement; - update = 1; + 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); } - if (update) { - rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); - } - } - return rv; -} - -/**************************************************************************************************/ -/* authz/challenge setup */ -/** - * Pre-Req: we have an account for the ACME server that has accepted the current license agreement - * For each domain in MD: - * - check if there already is a valid AUTHZ resource - * - if ot, create an AUTHZ resource with challenge data - */ -static apr_status_t ad_setup_authz(md_proto_driver_t *d) -{ - md_acme_driver_t *ad = d->baton; - apr_status_t rv; - md_t *md = ad->md; - md_acme_authz_t *authz; - int i; - int changed = 0; - - assert(ad->md); - assert(ad->acme); - - ad->phase = "check authz"; - - /* For each domain in MD: AUTHZ setup - * if an AUTHZ resource is known, check if it is still valid - * 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 - */ - rv = md_acme_authz_set_load(d->store, MD_SG_STAGING, md->name, &ad->authz_set, d->p); - if (!ad->authz_set || APR_STATUS_IS_ENOENT(rv)) { - ad->authz_set = md_acme_authz_set_create(d->p); - rv = APR_SUCCESS; - } - else if (APR_SUCCESS != rv) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: loading authz data", md->name); - md_acme_authz_set_purge(d->store, MD_SG_STAGING, d->p, md->name); - return APR_EAGAIN; - } - - /* Remove anything we no longer need */ - for (i = 0; i < ad->authz_set->authzs->nelts;) { - authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*); - if (!md_contains(md, authz->domain, 0)) { - md_acme_authz_set_remove(ad->authz_set, authz->domain); - changed = 1; - } - else { - ++i; - } - } - - /* Add anything we do not already have */ - for (i = 0; i < md->domains->nelts && APR_SUCCESS == rv; ++i) { - const char *domain = APR_ARRAY_IDX(md->domains, i, const char *); - authz = md_acme_authz_set_get(ad->authz_set, domain); - if (authz) { - /* check valid */ - rv = md_acme_authz_update(authz, ad->acme, d->store, d->p); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: updated authz for %s", - md->name, domain); - if (APR_SUCCESS != rv) { - md_acme_authz_set_remove(ad->authz_set, domain); - authz = NULL; - changed = 1; - } - } - if (!authz) { - /* create new one */ - rv = md_acme_authz_register(&authz, ad->acme, d->store, domain, d->p); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: created authz for %s", - md->name, domain); - if (APR_SUCCESS == rv) { - rv = md_acme_authz_set_add(ad->authz_set, authz); - changed = 1; + 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); + md_result_log(result, MD_LOG_ERR); } - } - } - - /* Save any changes */ - if (APR_SUCCESS == rv && changed) { - rv = md_acme_authz_set_save(d->store, d->p, MD_SG_STAGING, md->name, ad->authz_set, 0); - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, "%s: saved", md->name); - } - - return rv; -} - -/** - * Pre-Req: all domains have a AUTHZ resources at the ACME server - * For each domain in MD: - * - if AUTHZ resource is 'valid' -> continue - * - if AUTHZ resource is 'pending': - * - find preferred challenge choice - * - calculate challenge data for httpd to find - * - POST challenge start to ACME server - * For each domain in MD where AUTHZ is 'pending', until overall timeout: - * - wait a certain time, check status again - * If not all AUTHZ are valid, fail - */ -static apr_status_t ad_start_challenges(md_proto_driver_t *d) -{ - md_acme_driver_t *ad = d->baton; - apr_status_t rv = APR_SUCCESS; - md_acme_authz_t *authz; - int i, changed = 0; - - assert(ad->md); - assert(ad->acme); - assert(ad->authz_set); - - ad->phase = "start challenges"; - - for (i = 0; i < ad->authz_set->authzs->nelts && APR_SUCCESS == rv; ++i) { - authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: check AUTHZ for %s", - ad->md->name, authz->domain); - if (APR_SUCCESS != (rv = md_acme_authz_update(authz, ad->acme, d->store, d->p))) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: check authz for %s", - ad->md->name, authz->domain); - break; + goto leave; } - switch (authz->state) { - case MD_ACME_AUTHZ_S_VALID: - break; - - case MD_ACME_AUTHZ_S_PENDING: - rv = md_acme_authz_respond(authz, ad->acme, d->store, ad->ca_challenges, - d->md->pkey_spec, d->p); - changed = 1; - break; - - default: - rv = APR_EINVAL; - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, - "%s: unexpected AUTHZ state %d at %s", - authz->domain, authz->state, authz->location); - break; - } + md->ca_account = NULL; + update_md = 1; + update_acct = 1; } - if (APR_SUCCESS == rv && changed) { - rv = md_acme_authz_set_save(d->store, d->p, MD_SG_STAGING, ad->md->name, ad->authz_set, 0); - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, "%s: saved", ad->md->name); +leave: + /* Persist MD changes in STAGING, so we pick them up on next run */ + if (APR_SUCCESS == rv && update_md) { + rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); } - return rv; -} - -static apr_status_t check_challenges(void *baton, int attempt) -{ - md_proto_driver_t *d = baton; - md_acme_driver_t *ad = d->baton; - md_acme_authz_t *authz; - apr_status_t rv = APR_SUCCESS; - int i; - - for (i = 0; i < ad->authz_set->authzs->nelts && APR_SUCCESS == rv; ++i) { - authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: check AUTHZ for %s(%d. attempt)", - ad->md->name, authz->domain, attempt); - if (APR_SUCCESS == (rv = md_acme_authz_update(authz, ad->acme, d->store, d->p))) { - switch (authz->state) { - case MD_ACME_AUTHZ_S_VALID: - break; - case MD_ACME_AUTHZ_S_PENDING: - rv = APR_EAGAIN; - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, - "%s: status pending at %s", authz->domain, authz->location); - break; - default: - rv = APR_EINVAL; - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, - "%s: unexpected AUTHZ state %d at %s", - authz->domain, authz->state, authz->location); - break; - } - } + /* Persist account changes in STAGING, so we pick them up on next run */ + if (APR_SUCCESS == rv && update_acct) { + rv = save_acct_staged(ad->acme, d->store, md->name, d->p); } return rv; } -static apr_status_t ad_monitor_challenges(md_proto_driver_t *d) -{ - md_acme_driver_t *ad = d->baton; - apr_status_t rv; - - assert(ad->md); - assert(ad->acme); - assert(ad->authz_set); - - ad->phase = "monitor challenges"; - rv = md_util_try(check_challenges, d, 0, ad->authz_monitor_timeout, 0, 0, 1); - - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, d->p, - "%s: checked all domain authorizations", ad->md->name); - return rv; -} - /**************************************************************************************************/ /* poll cert */ @@ -352,41 +198,52 @@ static void get_up_link(md_proto_driver_t *d, apr_table_t *headers) { md_acme_driver_t *ad = d->baton; - ad->next_up_link = md_link_find_relation(headers, d->p, "up"); - if (ad->next_up_link) { + ad->chain_up_link = md_link_find_relation(headers, d->p, "up"); + if (ad->chain_up_link) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, - "server reports up link as %s", ad->next_up_link); + "server reports up link as %s", ad->chain_up_link); } } -static apr_status_t read_http_cert(md_cert_t **pcert, apr_pool_t *p, +static apr_status_t add_http_certs(apr_array_header_t *chain, apr_pool_t *p, const md_http_response_t *res) { apr_status_t rv = APR_SUCCESS; + const char *ct; - if (APR_SUCCESS != (rv = md_cert_read_http(pcert, p, res)) + ct = apr_table_get(res->headers, "Content-Type"); + ct = md_util_parse_ct(res->req->pool, ct); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p, + "parse certs from %s -> %d (%s)", res->req->url, res->status, ct); + if (ct && !strcmp("application/x-pkcs7-mime", ct)) { + /* this looks like a root cert and we do not want those in our chain */ + goto out; + } + + /* Lets try to read one or more certificates */ + if (APR_SUCCESS != (rv = md_cert_chain_read_http(chain, p, res)) && APR_STATUS_IS_ENOENT(rv)) { rv = APR_EAGAIN; md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "cert not in response from %s", res->req->url); } +out: return rv; } -static apr_status_t on_got_cert(md_acme_t *acme, const md_http_response_t *res, void *baton) +static apr_status_t on_add_cert(md_acme_t *acme, const md_http_response_t *res, void *baton) { md_proto_driver_t *d = baton; md_acme_driver_t *ad = d->baton; apr_status_t rv = APR_SUCCESS; + int count; (void)acme; - if (APR_SUCCESS == (rv = read_http_cert(&ad->cert, d->p, res))) { - rv = md_store_save(d->store, d->p, MD_SG_STAGING, ad->md->name, MD_FN_CERT, - MD_SV_CERT, ad->cert, 0); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "cert parsed and saved"); - if (APR_SUCCESS == rv) { - get_up_link(d, res->headers); - } + count = ad->cred->chain->nelts; + if (APR_SUCCESS == (rv = add_http_certs(ad->cred->chain, d->p, res))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%d certs parsed", + ad->cred->chain->nelts - count); + get_up_link(d, res->headers); } return rv; } @@ -397,19 +254,21 @@ static apr_status_t get_cert(void *baton, int attempt) md_acme_driver_t *ad = d->baton; (void)attempt; - return md_acme_GET(ad->acme, ad->md->cert_url, NULL, NULL, on_got_cert, d); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, d->p, "retrieving cert from %s", + ad->order->certificate); + return md_acme_GET(ad->acme, ad->order->certificate, NULL, NULL, on_add_cert, NULL, d); } -static apr_status_t ad_cert_poll(md_proto_driver_t *d, int only_once) +apr_status_t md_acme_drive_cert_poll(md_proto_driver_t *d, int only_once) { md_acme_driver_t *ad = d->baton; apr_status_t rv; assert(ad->md); assert(ad->acme); - assert(ad->md->cert_url); + assert(ad->order); + assert(ad->order->certificate); - ad->phase = "poll certificate"; if (only_once) { rv = get_cert(d, 0); } @@ -417,12 +276,12 @@ static apr_status_t ad_cert_poll(md_proto_driver_t *d, int only_once) rv = md_util_try(get_cert, d, 1, ad->cert_poll_timeout, 0, 0, 1); } - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "poll for cert at %s", ad->md->cert_url); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "poll for cert at %s", ad->order->certificate); return rv; } /**************************************************************************************************/ -/* cert setup */ +/* order finalization */ static apr_status_t on_init_csr_req(md_acme_req_t *req, void *baton) { @@ -431,7 +290,6 @@ static apr_status_t on_init_csr_req(md_acme_req_t *req, void *baton) md_json_t *jpayload; jpayload = md_json_create(req->p); - md_json_sets("new-cert", jpayload, MD_KEY_RESOURCE, NULL); md_json_sets(ad->csr_der_64, jpayload, MD_KEY_CSR, NULL); return md_acme_req_body_init(req, jpayload); @@ -441,34 +299,39 @@ static apr_status_t csr_req(md_acme_t *acme, const md_http_response_t *res, void { md_proto_driver_t *d = baton; md_acme_driver_t *ad = d->baton; + const char *location; + md_cert_t *cert; apr_status_t rv = APR_SUCCESS; (void)acme; - ad->md->cert_url = apr_table_get(res->headers, "location"); - if (!ad->md->cert_url) { + location = apr_table_get(res->headers, "location"); + if (!location) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, "cert created without giving its location header"); return APR_EINVAL; } - if (APR_SUCCESS != (rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0))) { + ad->order->certificate = apr_pstrdup(d->p, location); + if (APR_SUCCESS != (rv = md_acme_order_save(d->store, d->p, MD_SG_STAGING, + d->md->name, ad->order, 0))) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, - "%s: saving cert url %s", ad->md->name, ad->md->cert_url); + "%s: saving cert url %s", d->md->name, location); return rv; } /* Check if it already was sent with this response */ - ad->next_up_link = NULL; - if (APR_SUCCESS == (rv = md_cert_read_http(&ad->cert, d->p, res))) { - rv = md_cert_save(d->store, d->p, MD_SG_STAGING, ad->md->name, ad->cert, 0); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "cert parsed and saved"); - if (APR_SUCCESS == rv) { - get_up_link(d, res->headers); - } + ad->chain_up_link = NULL; + if (APR_SUCCESS == (rv = md_cert_read_http(&cert, d->p, res))) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "cert parsed"); + apr_array_clear(ad->cred->chain); + APR_ARRAY_PUSH(ad->cred->chain, md_cert_t*) = cert; + get_up_link(d, res->headers); } else if (APR_STATUS_IS_ENOENT(rv)) { rv = APR_SUCCESS; - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, - "cert not in response, need to poll %s", ad->md->cert_url); + if (location) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "cert not in response, need to poll %s", location); + } } return rv; @@ -477,6 +340,7 @@ static apr_status_t csr_req(md_acme_t *acme, const md_http_response_t *res, void /** * Pre-Req: all domains have been validated by the ACME server, e.g. all have AUTHZ * resources that have status 'valid' + * - acme_driver->cred keeps the credentials to setup (key spec) * - Setup private key, if not already there * - Generate a CSR with org, contact, etc * - Optionally enable must-staple OCSP extension @@ -487,38 +351,41 @@ static apr_status_t csr_req(md_acme_t *acme, const md_http_response_t *res, void * - GET cert chain * - store cert chain */ -static apr_status_t ad_setup_certificate(md_proto_driver_t *d) +apr_status_t md_acme_drive_setup_cred_chain(md_proto_driver_t *d, md_result_t *result) { md_acme_driver_t *ad = d->baton; + md_pkey_spec_t *spec; md_pkey_t *privkey; apr_status_t rv; - ad->phase = "setup cert privkey"; - - rv = md_pkey_load(d->store, MD_SG_STAGING, ad->md->name, &privkey, d->p); + md_result_activity_printf(result, "Finalizing order for %s", ad->md->name); + + assert(ad->cred); + spec = ad->cred->spec; + + rv = md_pkey_load(d->store, MD_SG_STAGING, d->md->name, spec, &privkey, d->p); if (APR_STATUS_IS_ENOENT(rv)) { - if (APR_SUCCESS == (rv = md_pkey_gen(&privkey, d->p, d->md->pkey_spec))) { - rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, ad->md->name, privkey, 1); + if (APR_SUCCESS == (rv = md_pkey_gen(&privkey, d->p, spec))) { + rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, d->md->name, spec, privkey, 1); } - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: generate privkey", ad->md->name); - } - - if (APR_SUCCESS == rv) { - ad->phase = "setup csr"; - rv = md_cert_req_create(&ad->csr_der_64, ad->md, privkey, d->p); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: create CSR", ad->md->name); - } - - if (APR_SUCCESS == rv) { - ad->phase = "submit csr"; - rv = md_acme_POST(ad->acme, ad->acme->new_cert, on_init_csr_req, NULL, csr_req, d); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "%s: generate %s privkey", d->md->name, md_pkey_spec_name(spec)); } + if (APR_SUCCESS != rv) goto leave; + + md_result_activity_printf(result, "Creating %s CSR", md_pkey_spec_name(spec)); + rv = md_cert_req_create(&ad->csr_der_64, d->md->name, ad->domains, + ad->md->must_staple, privkey, d->p); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: create %s CSR", + d->md->name, md_pkey_spec_name(spec)); + if (APR_SUCCESS != rv) goto leave; + + md_result_activity_printf(result, "Submitting %s CSR to CA", md_pkey_spec_name(spec)); + assert(ad->order->finalize); + rv = md_acme_POST(ad->acme, ad->order->finalize, on_init_csr_req, NULL, csr_req, NULL, d); - if (APR_SUCCESS == rv) { - if (!ad->cert) { - rv = ad_cert_poll(d, 0); - } - } +leave: + md_acme_report_result(ad->acme, rv, result); return rv; } @@ -530,22 +397,19 @@ static apr_status_t on_add_chain(md_acme_t *acme, const md_http_response_t *res, md_proto_driver_t *d = baton; md_acme_driver_t *ad = d->baton; apr_status_t rv = APR_SUCCESS; - md_cert_t *cert; const char *ct; (void)acme; ct = apr_table_get(res->headers, "Content-Type"); + ct = md_util_parse_ct(res->req->pool, ct); if (ct && !strcmp("application/x-pkcs7-mime", ct)) { /* root cert most likely, end it here */ return APR_SUCCESS; } - if (APR_SUCCESS == (rv = read_http_cert(&cert, d->p, res))) { + if (APR_SUCCESS == (rv = add_http_certs(ad->cred->chain, d->p, res))) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain cert parsed"); - APR_ARRAY_PUSH(ad->chain, md_cert_t *) = cert; - if (APR_SUCCESS == rv) { - get_up_link(d, res->headers); - } + get_up_link(d, res->headers); } return rv; } @@ -557,19 +421,32 @@ static apr_status_t get_chain(void *baton, int attempt) const char *prev_link = NULL; apr_status_t rv = APR_SUCCESS; - while (APR_SUCCESS == rv && ad->chain->nelts < 10) { - int nelts = ad->chain->nelts; + while (APR_SUCCESS == rv && ad->cred->chain->nelts < 10) { + int nelts = ad->cred->chain->nelts; - if (ad->next_up_link && (!prev_link || strcmp(prev_link, ad->next_up_link))) { - prev_link = ad->next_up_link; + if (ad->chain_up_link && (!prev_link || strcmp(prev_link, ad->chain_up_link))) { + prev_link = ad->chain_up_link; md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, - "next issuer is %s", ad->next_up_link); - rv = md_acme_GET(ad->acme, ad->next_up_link, NULL, NULL, on_add_chain, d); + "next chain cert at %s", ad->chain_up_link); + rv = md_acme_GET(ad->acme, ad->chain_up_link, NULL, NULL, on_add_chain, NULL, d); - if (APR_SUCCESS == rv && nelts == ad->chain->nelts) { + if (APR_SUCCESS == rv && nelts == ad->cred->chain->nelts) { break; } + else if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "error retrieving certificate from %s", ad->chain_up_link); + return rv; + } + } + else if (ad->cred->chain->nelts <= 1) { + /* This cannot be the complete chain (no one signs new web certs with their root) + * and we did not see a "Link: ...rel=up", so we do not know how to continue. */ + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "no link header 'up' for new certificate, unable to retrieve chain"); + rv = APR_EINVAL; + break; } else { rv = APR_SUCCESS; @@ -577,63 +454,103 @@ static apr_status_t get_chain(void *baton, int attempt) } } md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, - "got chain with %d certs (%d. attempt)", ad->chain->nelts, attempt); + "got chain with %d certs (%d. attempt)", ad->cred->chain->nelts, attempt); return rv; } -static apr_status_t ad_chain_install(md_proto_driver_t *d) +static apr_status_t ad_chain_retrieve(md_proto_driver_t *d) { md_acme_driver_t *ad = d->baton; apr_status_t rv; - /* We should have that from initial cert retrieval, but if we restarted - * or switched child process, we need to retrieve this again from the - * certificate resources. */ - if (!ad->next_up_link) { - if (APR_SUCCESS != (rv = ad_cert_poll(d, 0))) { - return rv; + /* This may be called repeatedly and needs to progress. The relevant state is in + * ad->cred->chain the certificate chain, starting with the new cert for the md + * ad->order->certificate the url where ACME offers us the new md certificate. This may + * be a single one or even the complete chain + * ad->chain_up_link in case the last certificate retrieval did not end the chain, + * the link header with relation "up" gives us the location + * for the next cert in the chain + */ + if (md_array_is_empty(ad->cred->chain)) { + /* Need to start at the order */ + ad->chain_up_link = NULL; + if (!ad->order) { + rv = APR_EGENERAL; + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, + "%s: asked to retrieve chain, but no order in context", d->md->name); + goto out; } - if (!ad->next_up_link) { + if (!ad->order->certificate) { + rv = APR_EGENERAL; md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, - "server reports no link header 'up' for certificate at %s", ad->md->cert_url); - return APR_EINVAL; + "%s: asked to retrieve chain, but no certificate url part of order", d->md->name); + goto out; + } + + if (APR_SUCCESS != (rv = md_acme_drive_cert_poll(d, 0))) { + goto out; } } - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, - "chain starts at %s", ad->next_up_link); - ad->chain = apr_array_make(d->p, 5, sizeof(md_cert_t *)); - if (APR_SUCCESS == (rv = md_util_try(get_chain, d, 0, ad->cert_poll_timeout, 0, 0, 0))) { - rv = md_store_save(d->store, d->p, MD_SG_STAGING, ad->md->name, MD_FN_CHAIN, - MD_SV_CHAIN, ad->chain, 0); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain fetched and saved"); - } + rv = md_util_try(get_chain, d, 0, ad->cert_poll_timeout, 0, 0, 0); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain retrieved"); + +out: return rv; } /**************************************************************************************************/ /* ACME driver init */ -static apr_status_t acme_driver_init(md_proto_driver_t *d) +static apr_status_t acme_driver_preload_init(md_proto_driver_t *d, md_result_t *result) { md_acme_driver_t *ad; - apr_status_t rv = APR_SUCCESS; - + md_credentials_t *cred; + int i; + + md_result_set(result, APR_SUCCESS, NULL); + ad = apr_pcalloc(d->p, sizeof(*ad)); d->baton = ad; - ad->driver = d; + ad->driver = d; ad->authz_monitor_timeout = apr_time_from_sec(30); ad->cert_poll_timeout = apr_time_from_sec(30); + ad->ca_challenges = apr_array_make(d->p, 3, sizeof(const char*)); + + /* We want to obtain credentials (key+certificate) for every key spec in this MD */ + ad->creds = apr_array_make(d->p, md_pkeys_spec_count(d->md->pks), sizeof(md_credentials_t*)); + for (i = 0; i < md_pkeys_spec_count(d->md->pks); ++i) { + cred = apr_pcalloc(d->p, sizeof(*cred)); + cred->spec = md_pkeys_spec_get(d->md->pks, i); + cred->chain = apr_array_make(d->p, 5, sizeof(md_cert_t*)); + APR_ARRAY_PUSH(ad->creds, md_credentials_t*) = cred; + } + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, result->status, d->p, + "%s: init_base driver", d->md->name); + return result->status; +} + +static apr_status_t acme_driver_init(md_proto_driver_t *d, md_result_t *result) +{ + md_acme_driver_t *ad; + int dis_http, dis_https, dis_alpn_acme, dis_dns; + const char *challenge; + + acme_driver_preload_init(d, result); + md_result_set(result, APR_SUCCESS, NULL); + if (APR_SUCCESS != result->status) goto leave; + + ad = d->baton; /* We can only support challenges if the server is reachable from the outside * via port 80 and/or 443. These ports might be mapped for httpd to something * else, but a mapping needs to exist. */ - ad->ca_challenges = apr_array_make(d->p, 3, sizeof(const char *)); - if (d->challenge) { - /* we have been told to use this type */ - APR_ARRAY_PUSH(ad->ca_challenges, const char*) = apr_pstrdup(d->p, d->challenge); + challenge = apr_table_get(d->env, MD_KEY_CHALLENGE); + if (challenge) { + APR_ARRAY_PUSH(ad->ca_challenges, const char*) = apr_pstrdup(d->p, challenge); } else if (d->md->ca_challenges && d->md->ca_challenges->nelts > 0) { /* pre-configured set for this managed domain */ @@ -641,56 +558,119 @@ static apr_status_t acme_driver_init(md_proto_driver_t *d) } else { /* free to chose. Add all we support and see what we get offered */ + APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_TLSALPN01; APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_HTTP01; - APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_TLSSNI01; - } - - if (!d->can_http && !d->can_https) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, d->p, "%s: the server seems neither " - "reachable via http (port 80) nor https (port 443). The ACME protocol " - "needs at least one of those so the CA can talk to the server and verify " - "a domain ownership.", d->md->name); - return APR_EGENERAL; - } - - if (!d->can_http) { - ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_HTTP01, 0); - } - if (!d->can_https) { - ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_TLSSNI01, 0); - } + APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_DNS01; + + if (!d->can_http && !d->can_https + && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0, 0) < 0) { + md_result_printf(result, APR_EGENERAL, + "the server seems neither reachable via http (port 80) nor https (port 443). " + "Please look at the MDPortMap configuration directive on how to correct this. " + "The ACME protocol needs at least one of those so the CA can talk to the server " + "and verify a domain ownership. Alternatively, you may configure support " + "for the %s challenge directive.", MD_AUTHZ_TYPE_DNS01); + goto leave; + } - if (apr_is_empty_array(ad->ca_challenges)) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, d->p, "%s: specific CA challenge methods " - "have been configured, but the server is unable to use any of those. " - "For 'http-01' it needs to be reachable on port 80, for 'tls-sni-01'" - " port 443 is needed.", d->md->name); - return APR_EGENERAL; + dis_http = dis_https = dis_alpn_acme = dis_dns = 0; + if (!d->can_http && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_HTTP01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_HTTP01, 0); + dis_http = 1; + } + if (!d->can_https && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0); + dis_https = 1; + } + if (apr_is_empty_array(d->md->acme_tls_1_domains) + && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_TLSALPN01, 0); + dis_alpn_acme = 1; + } + if (!apr_table_get(d->env, MD_KEY_CMD_DNS01) + && NULL == d->md->dns01_cmd + && md_array_str_index(ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0, 1) >= 0) { + ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_DNS01, 0); + dis_dns = 1; + } + + if (apr_is_empty_array(ad->ca_challenges)) { + md_result_printf(result, APR_EGENERAL, + "None of the ACME challenge methods configured for this domain are suitable.%s%s%s%s", + dis_http? " The http: challenge 'http-01' is disabled because the server seems not reachable on public port 80." : "", + dis_https? " The https: challenge 'tls-alpn-01' is disabled because the server seems not reachable on public port 443." : "", + dis_alpn_acme? " The https: challenge 'tls-alpn-01' is disabled because the Protocols configuration does not include the 'acme-tls/1' protocol." : "", + dis_dns? " The DNS challenge 'dns-01' is disabled because the directive 'MDChallengeDns01' is not configured." : "" + ); + goto leave; + } } - - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, d->p, "%s: init driver", d->md->name); - - return rv; + + md_result_printf(result, 0, "MDomain %s initialized with support for ACME challenges %s", + d->md->name, apr_array_pstrcat(d->p, ad->ca_challenges, ' ')); + +leave: + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, result->status, d->p, "%s: init driver", d->md->name); + return result->status; } /**************************************************************************************************/ /* ACME staging */ -static apr_status_t acme_stage(md_proto_driver_t *d) +static apr_status_t load_missing_creds(md_proto_driver_t *d) +{ + md_acme_driver_t *ad = d->baton; + md_credentials_t *cred; + apr_array_header_t *chain; + int i, complete; + apr_status_t rv; + + complete = 1; + for (i = 0; i < ad->creds->nelts; ++i) { + rv = APR_SUCCESS; + cred = APR_ARRAY_IDX(ad->creds, i, md_credentials_t*); + if (!cred->pkey) { + rv = md_pkey_load(d->store, MD_SG_STAGING, d->md->name, cred->spec, &cred->pkey, d->p); + } + if (APR_SUCCESS == rv && md_array_is_empty(cred->chain)) { + rv = md_pubcert_load(d->store, MD_SG_STAGING, d->md->name, cred->spec, &chain, d->p); + if (APR_SUCCESS == rv) { + apr_array_cat(cred->chain, chain); + } + } + if (APR_SUCCESS == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, d->p, "%s: credentials staged for %s certificate", + d->md->name, md_pkey_spec_name(cred->spec)); + } + else { + complete = 0; + } + } + return complete? APR_SUCCESS : APR_EAGAIN; +} + +static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result) { md_acme_driver_t *ad = d->baton; int reset_staging = d->reset; apr_status_t rv = APR_SUCCESS; - int renew = 1; - - if (md_log_is_level(d->p, MD_LOG_DEBUG)) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: staging started, " - "state=%d, can_http=%d, can_https=%d, challenges='%s'", - d->md->name, d->md->state, d->can_http, d->can_https, - apr_array_pstrcat(d->p, ad->ca_challenges, ' ')); + apr_time_t now, t, t2; + md_credentials_t *cred; + const char *ca_effective = NULL; + char ts[APR_RFC822_DATE_LEN]; + int i, first = 0; + + if (!d->md->ca_urls || d->md->ca_urls->nelts <= 0) { + /* No CA defined? This is checked in several other places, but lets be sure */ + md_result_printf(result, APR_INCOMPLETE, + "The managed domain %s is missing MDCertificateAuthority", d->md->name); + goto out; } + /* When not explicitly told to reset, we check the existing data. If + * it is incomplete or old, we trigger the reset for a clean start. */ if (!reset_staging) { + md_result_activity_setn(result, "Checking staging area"); rv = md_load(d->store, MD_SG_STAGING, d->md->name, &ad->md, d->p); if (APR_SUCCESS == rv) { /* So, we have a copy in staging, but is it a recent or an old one? */ @@ -702,318 +682,420 @@ static apr_status_t acme_stage(md_proto_driver_t *d) reset_staging = 1; rv = APR_SUCCESS; } - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, - "%s: checked staging area, will%s reset", - d->md->name, reset_staging? "" : " not"); } - + + /* What CA are we using this time? */ + if (ad->md && ad->md->ca_effective) { + /* There was one chosen on the previous run. Do we stick to it? */ + ca_effective = ad->md->ca_effective; + if (d->md->ca_urls->nelts > 1 && d->attempt >= d->retry_failover) { + /* We have more than one CA to choose from and this is the (at least) + * third attempt with the same CA. Let's switch to the next one. */ + int last_idx = md_array_str_index(d->md->ca_urls, ca_effective, 0, 1); + if (last_idx >= 0) { + int next_idx = (last_idx+1) % d->md->ca_urls->nelts; + ca_effective = APR_ARRAY_IDX(d->md->ca_urls, next_idx, const char*); + } + else { + /* not part of current configuration? */ + ca_effective = NULL; + } + /* switching CA means we need to wipe the staging area */ + reset_staging = 1; + } + } + + if (!ca_effective) { + /* None chosen yet, pick the first one configured */ + ca_effective = APR_ARRAY_IDX(d->md->ca_urls, 0, const char*); + } + + if (md_log_is_level(d->p, MD_LOG_DEBUG)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: staging started, " + "state=%d, attempt=%d, acme=%s, challenges='%s'", + d->md->name, d->md->state, d->attempt, ca_effective, + apr_array_pstrcat(d->p, ad->ca_challenges, ' ')); + } + if (reset_staging) { + md_result_activity_setn(result, "Resetting staging area"); /* reset the staging area for this domain */ rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "%s: reset staging area", d->md->name); if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) { - return rv; + md_result_printf(result, rv, "resetting staging area"); + goto out; } rv = APR_SUCCESS; ad->md = NULL; + ad->order = NULL; } - if (ad->md && ad->md->state == MD_S_MISSING) { - /* There is config information missing. It makes no sense to drive this MD further */ - rv = APR_INCOMPLETE; + md_result_activity_setn(result, "Assessing current status"); + if (ad->md && ad->md->state == MD_S_MISSING_INFORMATION) { + /* ToS agreement is missing. It makes no sense to drive this MD further */ + md_result_printf(result, APR_INCOMPLETE, + "The managed domain %s is missing required information", d->md->name); goto out; } - if (ad->md) { - /* staging in progress. look for new ACME account information collected there */ - rv = md_reg_creds_get(&ad->ncreds, d->reg, MD_SG_STAGING, d->md, d->p); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: checked creds", d->md->name); - if (APR_STATUS_IS_ENOENT(rv)) { - rv = APR_SUCCESS; - } + if (ad->md && APR_SUCCESS == load_missing_creds(d)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: all credentials staged", d->md->name); + goto ready; } - /* Find out where we're at with this managed domain */ - if (ad->ncreds && ad->ncreds->privkey && ad->ncreds->pubcert) { - /* There is a full set staged, to be loaded */ - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: all data staged", d->md->name); - renew = 0; + /* Need to renew */ + if (!ad->md || !md_array_str_eq(ad->md->ca_urls, d->md->ca_urls, 1)) { + md_result_activity_printf(result, "Resetting staging for %s", d->md->name); + /* re-initialize staging */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: setup staging", d->md->name); + md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); + ad->md = md_copy(d->p, d->md); + ad->md->ca_effective = ca_effective; + ad->md->ca_account = NULL; + ad->order = NULL; + rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Saving MD information in staging area."); + md_result_log(result, MD_LOG_ERR); + goto out; + } + } + if (!ad->domains) { + ad->domains = md_dns_make_minimal(d->p, ad->md->domains); } - if (renew) { - if (APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, d->md->ca_url, d->proxy_url)) - || APR_SUCCESS != (rv = md_acme_setup(ad->acme))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, "%s: setup ACME(%s)", - d->md->name, d->md->ca_url); - return rv; - } - - if (!ad->md) { - /* re-initialize staging */ - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: setup staging", d->md->name); - md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); - ad->md = md_copy(d->p, d->md); - ad->md->cert_url = NULL; /* do not retrieve the old cert */ - rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: save staged md", - ad->md->name); - } - - if (APR_SUCCESS == rv && !ad->cert) { - md_cert_load(d->store, MD_SG_STAGING, ad->md->name, &ad->cert, d->p); - } + md_result_activity_printf(result, "Contacting ACME server for %s at %s", + d->md->name, ca_effective); + if (APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, ca_effective, + d->proxy_url, d->ca_file))) { + md_result_printf(result, rv, "setup ACME communications"); + md_result_log(result, MD_LOG_ERR); + goto out; + } + if (APR_SUCCESS != (rv = md_acme_setup(ad->acme, result))) { + md_result_log(result, MD_LOG_ERR); + goto out; + } - if (APR_SUCCESS == rv && !ad->cert) { - ad->phase = "get certificate"; - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: need certificate", d->md->name); - - /* Chose (or create) and ACME account to use */ - rv = ad_set_acct(d); - - /* Check that the account agreed to the terms-of-service, otherwise - * requests for new authorizations are denied. ToS may change during the - * lifetime of an account */ - if (APR_SUCCESS == rv) { - const char *required; - - ad->phase = "check agreement"; - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, - "%s: check Terms-of-Service agreement", d->md->name); - - rv = md_acme_check_agreement(ad->acme, d->p, ad->md->ca_agreement, &required); - - if (APR_STATUS_IS_INCOMPLETE(rv) && required) { - /* The CA wants the user to agree to Terms-of-Services. Until the user - * has reconfigured and restarted the server, this MD cannot be - * driven further */ - ad->md->state = MD_S_MISSING; - md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); - - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, - "%s: the CA requires you to accept the terms-of-service " - "as specified in <%s>. " - "Please read the document that you find at that URL and, " - "if you agree to the conditions, configure " - "\"MDCertificateAgreement url\" " - "with exactly that URL in your Apache. " - "Then (graceful) restart the server to activate.", - ad->md->name, required); - goto out; + if (APR_SUCCESS != load_missing_creds(d)) { + for (i = 0; i < ad->creds->nelts; ++i) { + ad->cred = APR_ARRAY_IDX(ad->creds, i, md_credentials_t*); + if (!ad->cred->pkey || md_array_is_empty(ad->cred->chain)) { + md_result_activity_printf(result, "Driving ACME to renew %s certificate for %s", + md_pkey_spec_name(ad->cred->spec),d->md->name); + /* The process of setting up challenges and verifying domain + * names differs between ACME versions. */ + switch (MD_ACME_VERSION_MAJOR(ad->acme->version)) { + case 1: + md_result_printf(result, APR_EINVAL, + "ACME server speaks version 1, an obsolete version of the ACME " + "protocol that is no longer supported."); + rv = result->status; + break; + default: + /* In principle, we only know ACME version 2. But we assume + that a new protocol which announces a directory with all members + from version 2 will act backward compatible. + This is, of course, an assumption... + */ + rv = md_acmev2_drive_renew(ad, d, result); + break; } - } - - /* If we know a cert's location, try to get it. Previous download might - * have failed. If server 404 it, we clear our memory of it. */ - if (APR_SUCCESS == rv && ad->md->cert_url) { - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, - "%s: polling certificate", d->md->name); - rv = ad_cert_poll(d, 1); - if (APR_STATUS_IS_ENOENT(rv)) { - /* Server reports to know nothing about it. */ - ad->md->cert_url = NULL; - rv = md_reg_update(d->reg, d->p, ad->md->name, ad->md, MD_UPD_CERT_URL); - } - } - - if (APR_SUCCESS == rv && !ad->cert) { - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, - "%s: setup new authorization", d->md->name); - if (APR_SUCCESS != (rv = ad_setup_authz(d))) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: setup authz resource", - ad->md->name); - goto out; - } - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, - "%s: setup new challenges", d->md->name); - if (APR_SUCCESS != (rv = ad_start_challenges(d))) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: start challenges", - ad->md->name); - goto out; - } - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, - "%s: monitoring challenge status", d->md->name); - if (APR_SUCCESS != (rv = ad_monitor_challenges(d))) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: monitor challenges", - ad->md->name); - goto out; - } - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, - "%s: creating certificate request", d->md->name); - if (APR_SUCCESS != (rv = ad_setup_certificate(d))) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: setup certificate", - ad->md->name); - goto out; + if (APR_SUCCESS != rv) goto out; + + if (md_array_is_empty(ad->cred->chain) || ad->chain_up_link) { + md_result_activity_printf(result, "Retrieving %s certificate chain for %s", + md_pkey_spec_name(ad->cred->spec), d->md->name); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, + "%s: retrieving %s certificate chain", + d->md->name, md_pkey_spec_name(ad->cred->spec)); + rv = ad_chain_retrieve(d); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Unable to retrieve %s certificate chain.", + md_pkey_spec_name(ad->cred->spec)); + goto out; + } + + if (!md_array_is_empty(ad->cred->chain)) { + + if (!ad->cred->pkey) { + rv = md_pkey_load(d->store, MD_SG_STAGING, d->md->name, ad->cred->spec, &ad->cred->pkey, d->p); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Loading the private key."); + goto out; + } + } + + if (ad->cred->pkey) { + rv = md_check_cert_and_pkey(ad->cred->chain, ad->cred->pkey); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Certificate and private key do not match."); + + /* Delete the order */ + md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env); + + goto out; + } + } + + rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, d->md->name, + ad->cred->spec, ad->cred->chain, 0); + if (APR_SUCCESS != rv) { + md_result_printf(result, rv, "Saving new %s certificate chain.", + md_pkey_spec_name(ad->cred->spec)); + goto out; + } + } } - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, - "%s: received certificate", d->md->name); + + /* Clean up the order, so the next pkey spec sets up a new one */ + md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env); } - } + } + + + /* As last step, cleanup any order we created so that challenge data + * may be removed asap. */ + md_acme_order_purge(d->store, d->p, MD_SG_STAGING, d->md, d->env); + + /* first time this job ran through */ + first = 1; +ready: + md_result_activity_setn(result, NULL); + /* we should have the complete cert chain now */ + assert(APR_SUCCESS == load_missing_creds(d)); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "%s: certificates ready, activation delay set to %s", + d->md->name, md_duration_format(d->p, d->activation_delay)); + + /* determine when it should be activated */ + t = apr_time_now(); + for (i = 0; i < ad->creds->nelts; ++i) { + cred = APR_ARRAY_IDX(ad->creds, i, md_credentials_t*); + t2 = md_cert_get_not_before(APR_ARRAY_IDX(cred->chain, 0, md_cert_t*)); + if (t2 > t) t = t2; + } + md_result_delay_set(result, t); + + /* If the existing MD is complete and un-expired, delay the activation + * to 24 hours after new cert is valid (if there is enough time left), so + * that cients with skewed clocks do not see a problem. */ + now = apr_time_now(); + if (d->md->state == MD_S_COMPLETE) { + apr_time_t valid_until, delay_activation; - if (APR_SUCCESS == rv && !ad->chain) { - /* have we created this already? */ - md_chain_load(d->store, MD_SG_STAGING, ad->md->name, &ad->chain, d->p); - } - if (APR_SUCCESS == rv && !ad->chain) { - ad->phase = "install chain"; - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, - "%s: retrieving certificate chain", d->md->name); - rv = ad_chain_install(d); - } - - if (APR_SUCCESS == rv && !ad->pubcert) { - /* have we created this already? */ - md_pubcert_load(d->store, MD_SG_STAGING, ad->md->name, &ad->pubcert, d->p); - } - if (APR_SUCCESS == rv && !ad->pubcert) { - /* combine cert + chain into the pubcert */ - ad->pubcert = apr_array_make(d->p, ad->chain->nelts + 1, sizeof(md_cert_t*)); - APR_ARRAY_PUSH(ad->pubcert, md_cert_t *) = ad->cert; - apr_array_cat(ad->pubcert, ad->chain); - rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, ad->md->name, ad->pubcert, 0); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, + "%s: state is COMPLETE, checking existing certificates", d->md->name); + valid_until = md_reg_valid_until(d->reg, d->md, d->p); + if (d->activation_delay < 0) { + /* special simulation for test case */ + if (first) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, + "%s: delay ready_at to now+1s", d->md->name); + md_result_delay_set(result, apr_time_now() + apr_time_from_sec(1)); + } } - - if (APR_SUCCESS == rv && ad->cert) { - apr_time_t now = apr_time_now(); - apr_interval_time_t max_delay, delay_activation; - - /* determine when this cert should be activated */ - d->stage_valid_from = md_cert_get_not_before(ad->cert); - if (d->md->state == MD_S_COMPLETE && d->md->expires > now) { - /** - * The MD is complete and un-expired. This is a renewal run. - * Give activation 24 hours leeway (if we have that time) to - * accommodate for clients with somewhat weird clocks. - */ - delay_activation = apr_time_from_sec(MD_SECS_PER_DAY); - if (delay_activation > (max_delay = d->md->expires - now)) { - delay_activation = max_delay; - } - d->stage_valid_from += delay_activation; + else if (valid_until > now) { + delay_activation = d->activation_delay; + if (delay_activation > (valid_until - now)) { + delay_activation = (valid_until - now); } + md_result_delay_set(result, result->ready_at + delay_activation); } } -out: + + /* There is a full set staged, to be loaded */ + apr_rfc822_date(ts, result->ready_at); + if (result->ready_at > now) { + md_result_printf(result, APR_SUCCESS, + "The certificate for the managed domain has been renewed successfully and can " + "be used from %s on.", ts); + } + else { + md_result_printf(result, APR_SUCCESS, + "The certificate for the managed domain has been renewed successfully and can " + "be used (valid since %s). A graceful server restart now is recommended.", ts); + } + +out: return rv; } -static apr_status_t acme_driver_stage(md_proto_driver_t *d) +static apr_status_t acme_driver_renew(md_proto_driver_t *d, md_result_t *result) { - md_acme_driver_t *ad = d->baton; apr_status_t rv; - ad->phase = "ACME staging"; - if (APR_SUCCESS == (rv = acme_stage(d))) { - ad->phase = "staging done"; - } - - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: %s, %s", - d->md->name, d->proto->protocol, ad->phase); + rv = acme_renew(d, result); + md_result_log(result, MD_LOG_DEBUG); return rv; } /**************************************************************************************************/ /* ACME preload */ -static apr_status_t acme_preload(md_store_t *store, md_store_group_t load_group, - const char *name, const char *proxy_url, apr_pool_t *p) +static apr_status_t acme_preload(md_proto_driver_t *d, md_store_group_t load_group, + const char *name, md_result_t *result) { apr_status_t rv; - md_pkey_t *privkey, *acct_key; + md_pkey_t *acct_key; md_t *md; - apr_array_header_t *pubcert; + md_pkey_spec_t *pkspec; + 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, p, "%s: preload start", name); - /* Load all data which will be taken into the DOMAIN storage group. + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: preload start", name); + /* Load data from MD_SG_STAGING and save it into "load_group". * This serves several purposes: * 1. It's a format check on the input data. * 2. We write back what we read, creating data with our own access permissions * 3. We ignore any other accumulated data in STAGING - * 4. Once TMP is verified, we can swap/archive groups with a rename + * 4. Once "load_group" is complete an ok, we can swap/archive groups with a rename * 5. Reading/Writing the data will apply/remove any group specific data encryption. - * With the exemption that DOMAINS and TMP must apply the same policy/keys. */ - if (APR_SUCCESS != (rv = md_load(store, MD_SG_STAGING, name, &md, p))) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading md json", name); - return rv; + if (APR_SUCCESS != (rv = md_load(d->store, MD_SG_STAGING, name, &md, d->p))) { + md_result_set(result, rv, "loading staged md.json"); + goto leave; } - if (APR_SUCCESS != (rv = md_pkey_load(store, MD_SG_STAGING, name, &privkey, p))) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading staging private key", name); - return rv; - } - if (APR_SUCCESS != (rv = md_pubcert_load(store, MD_SG_STAGING, name, &pubcert, p))) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading pubcert", name); - return rv; + if (!md->ca_effective) { + rv = APR_ENOENT; + md_result_set(result, rv, "effective CA url not set"); + goto leave; } + all_creds = apr_array_make(d->p, 5, sizeof(md_credentials_t*)); + for (i = 0; i < md_pkeys_spec_count(md->pks); ++i) { + pkspec = md_pkeys_spec_get(md->pks, i); + if (APR_SUCCESS != (rv = md_creds_load(d->store, MD_SG_STAGING, name, pkspec, &creds, d->p))) { + md_result_printf(result, rv, "loading staged credentials #%d", i); + goto leave; + } + if (!creds->chain) { + rv = APR_ENOENT; + md_result_printf(result, rv, "no certificate in staged credentials #%d", i); + goto leave; + } + if (APR_SUCCESS != (rv = md_check_cert_and_pkey(creds->chain, creds->pkey))) { + md_result_printf(result, rv, "certificate and private key do not match in staged credentials #%d", i); + goto leave; + } + APR_ARRAY_PUSH(all_creds, md_credentials_t*) = creds; + } + /* See if staging holds a new or modified account data */ - rv = md_acme_acct_load(&acct, &acct_key, store, MD_SG_STAGING, name, p); + rv = md_acme_acct_load(&acct, &acct_key, d->store, MD_SG_STAGING, name, d->p); if (APR_STATUS_IS_ENOENT(rv)) { acct = NULL; acct_key = NULL; rv = APR_SUCCESS; } else if (APR_SUCCESS != rv) { - return rv; + md_result_set(result, rv, "loading staged account"); + goto leave; } - /* Remove any authz information we have here or in MD_SG_CHALLENGES */ - md_acme_authz_set_purge(store, MD_SG_STAGING, p, name); + md_result_activity_setn(result, "purging order information"); + md_acme_order_purge(d->store, d->p, MD_SG_STAGING, md, d->env); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, - "%s: staged data load, purging tmp space", name); - rv = md_store_purge(store, p, load_group, name); + md_result_activity_setn(result, "purging store tmp space"); + rv = md_store_purge(d->store, d->p, load_group, name); if (APR_SUCCESS != rv) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: error purging preload storage", name); - return rv; + md_result_set(result, rv, NULL); + goto leave; } if (acct) { md_acme_t *acme; + + /* 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 new account in their own STAGING area. By checking for accounts with + * the same url, we save them all into a single one. + */ + md_result_activity_setn(result, "saving staged account"); + 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; + } + else if (APR_SUCCESS != rv) { + md_result_set(result, rv, "error searching for existing account by url"); + goto leave; + } + } + + if (APR_SUCCESS != (rv = md_acme_create(&acme, d->p, md->ca_effective, + d->proxy_url, d->ca_file))) { + md_result_set(result, rv, "error setting up acme"); + goto leave; + } - if (APR_SUCCESS != (rv = md_acme_create(&acme, p, md->ca_url, proxy_url)) - || APR_SUCCESS != (rv = md_acme_acct_save(store, p, acme, acct, acct_key))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: error saving acct", name); - return rv; + if (APR_SUCCESS != (rv = md_acme_acct_save(d->store, d->p, acme, &id, acct, acct_key))) { + md_result_set(result, rv, "error saving account"); + goto leave; } - md->ca_account = acct->id; - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: saved ACME account %s", - name, acct->id); + md->ca_account = id; } - - if (APR_SUCCESS != (rv = md_save(store, p, load_group, md, 1))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving md json", name); - return rv; + 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; + } } - if (APR_SUCCESS != (rv = md_pubcert_save(store, p, load_group, name, pubcert, 1))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving cert chain", name); - return rv; + + md_result_activity_setn(result, "saving staged md/privkey/pubcert"); + if (APR_SUCCESS != (rv = md_save(d->store, d->p, load_group, md, 1))) { + md_result_set(result, rv, "writing md.json"); + goto leave; } - if (APR_SUCCESS != (rv = md_pkey_save(store, p, load_group, name, privkey, 1))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving private key", name); - return rv; + + for (i = 0; i < all_creds->nelts; ++i) { + creds = APR_ARRAY_IDX(all_creds, i, md_credentials_t*); + if (APR_SUCCESS != (rv = md_creds_save(d->store, d->p, load_group, name, creds, 1))) { + md_result_printf(result, rv, "writing credentials #%d", i); + goto leave; + } } + md_result_set(result, APR_SUCCESS, "saved staged data successfully"); + +leave: + md_result_log(result, MD_LOG_DEBUG); return rv; } -static apr_status_t acme_driver_preload(md_proto_driver_t *d, md_store_group_t group) +static apr_status_t acme_driver_preload(md_proto_driver_t *d, + md_store_group_t group, md_result_t *result) { - md_acme_driver_t *ad = d->baton; apr_status_t rv; - ad->phase = "ACME preload"; - if (APR_SUCCESS == (rv = acme_preload(d->store, group, d->md->name, d->proxy_url, d->p))) { - ad->phase = "preload done"; - } - - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: %s, %s", - d->md->name, d->proto->protocol, ad->phase); + rv = acme_preload(d, group, d->md->name, result); + md_result_log(result, MD_LOG_DEBUG); return rv; } +static apr_status_t acme_complete_md(md_t *md, apr_pool_t *p) +{ + (void)p; + if (!md->ca_urls || apr_is_empty_array(md->ca_urls)) { + md->ca_urls = apr_array_make(p, 3, sizeof(const char *)); + APR_ARRAY_PUSH(md->ca_urls, const char*) = MD_ACME_DEF_URL; + } + return APR_SUCCESS; +} + static md_proto_t ACME_PROTO = { - MD_PROTO_ACME, acme_driver_init, acme_driver_stage, acme_driver_preload + MD_PROTO_ACME, acme_driver_init, acme_driver_renew, + acme_driver_preload_init, acme_driver_preload, + acme_complete_md, }; apr_status_t md_acme_protos_add(apr_hash_t *protos, apr_pool_t *p) |