diff options
Diffstat (limited to 'modules/md/md_acme.c')
-rw-r--r-- | modules/md/md_acme.c | 724 |
1 files changed, 495 insertions, 229 deletions
diff --git a/modules/md/md_acme.c b/modules/md/md_acme.c index 3fbd365..4366bf6 100644 --- a/modules/md/md_acme.c +++ b/modules/md/md_acme.c @@ -30,6 +30,7 @@ #include "md_http.h" #include "md_log.h" #include "md_store.h" +#include "md_result.h" #include "md_util.h" #include "md_version.h" @@ -37,34 +38,36 @@ #include "md_acme_acct.h" -static const char *base_product; +static const char *base_product= "-"; typedef struct acme_problem_status_t acme_problem_status_t; struct acme_problem_status_t { - const char *type; - apr_status_t rv; + const char *type; /* the ACME error string */ + apr_status_t rv; /* what Apache status code we give it */ + int input_related; /* if error indicates wrong input value */ }; static acme_problem_status_t Problems[] = { - { "acme:error:badCSR", APR_EINVAL }, - { "acme:error:badNonce", APR_EAGAIN }, - { "acme:error:badSignatureAlgorithm", APR_EINVAL }, - { "acme:error:invalidContact", APR_BADARG }, - { "acme:error:unsupportedContact", APR_EGENERAL }, - { "acme:error:malformed", APR_EINVAL }, - { "acme:error:rateLimited", APR_BADARG }, - { "acme:error:rejectedIdentifier", APR_BADARG }, - { "acme:error:serverInternal", APR_EGENERAL }, - { "acme:error:unauthorized", APR_EACCES }, - { "acme:error:unsupportedIdentifier", APR_BADARG }, - { "acme:error:userActionRequired", APR_EAGAIN }, - { "acme:error:badRevocationReason", APR_EINVAL }, - { "acme:error:caa", APR_EGENERAL }, - { "acme:error:dns", APR_EGENERAL }, - { "acme:error:connection", APR_EGENERAL }, - { "acme:error:tls", APR_EGENERAL }, - { "acme:error:incorrectResponse", APR_EGENERAL }, + { "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 }, + { "acme:error:rateLimited", APR_BADARG, 0 }, + { "acme:error:rejectedIdentifier", APR_BADARG, 1 }, + { "acme:error:serverInternal", APR_EGENERAL, 0 }, + { "acme:error:unauthorized", APR_EACCES, 0 }, + { "acme:error:unsupportedIdentifier", APR_BADARG, 1 }, + { "acme:error:userActionRequired", APR_EAGAIN, 0 }, + { "acme:error:badRevocationReason", APR_EINVAL, 1 }, + { "acme:error:caa", APR_EGENERAL, 0 }, + { "acme:error:dns", APR_EGENERAL, 0 }, + { "acme:error:connection", APR_EGENERAL, 0 }, + { "acme:error:tls", APR_EGENERAL, 0 }, + { "acme:error:incorrectResponse", APR_EGENERAL, 0 }, }; static apr_status_t problem_status_get(const char *type) { @@ -85,89 +88,23 @@ static apr_status_t problem_status_get(const char *type) { return APR_EGENERAL; } -apr_status_t md_acme_init(apr_pool_t *p, const char *base) -{ - base_product = base; - return md_crypt_init(p); -} +int md_acme_problem_is_input_related(const char *problem) { + size_t i; -apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url, - const char *proxy_url) -{ - md_acme_t *acme; - const char *err = NULL; - apr_status_t rv; - apr_uri_t uri_parsed; - size_t len; - - if (!url) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, "create ACME without url"); - return APR_EINVAL; - } - - if (APR_SUCCESS != (rv = md_util_abs_uri_check(p, url, &err))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "invalid ACME uri (%s): %s", err, url); - return rv; + if (!problem) return 0; + if (strstr(problem, "urn:ietf:params:") == problem) { + problem += strlen("urn:ietf:params:"); } - - acme = apr_pcalloc(p, sizeof(*acme)); - acme->url = url; - acme->p = p; - acme->user_agent = apr_psprintf(p, "%s mod_md/%s", - base_product, MOD_MD_VERSION); - acme->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL; - acme->max_retries = 3; - - if (APR_SUCCESS != (rv = apr_uri_parse(p, url, &uri_parsed))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "parsing ACME uri: %s", url); - return APR_EINVAL; + else if (strstr(problem, "urn:") == problem) { + problem += strlen("urn:"); } - - len = strlen(uri_parsed.hostname); - acme->sname = (len <= 16)? uri_parsed.hostname : apr_pstrdup(p, uri_parsed.hostname + len - 16); - - *pacme = (APR_SUCCESS == rv)? acme : NULL; - return rv; -} -apr_status_t md_acme_setup(md_acme_t *acme) -{ - apr_status_t rv; - md_json_t *json; - - assert(acme->url); - if (!acme->http && APR_SUCCESS != (rv = md_http_create(&acme->http, acme->p, - acme->user_agent, acme->proxy_url))) { - return rv; - } - md_http_set_response_limit(acme->http, 1024*1024); - - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "get directory from %s", acme->url); - - rv = md_acme_get_json(&json, acme, acme->url, acme->p); - if (APR_SUCCESS == rv) { - acme->new_authz = md_json_gets(json, "new-authz", NULL); - acme->new_cert = md_json_gets(json, "new-cert", NULL); - acme->new_reg = md_json_gets(json, "new-reg", NULL); - acme->revoke_cert = md_json_gets(json, "revoke-cert", NULL); - if (acme->new_authz && acme->new_cert && acme->new_reg && acme->revoke_cert) { - return APR_SUCCESS; + for(i = 0; i < (sizeof(Problems)/sizeof(Problems[0])); ++i) { + if (!apr_strnatcasecmp(problem, Problems[i].type)) { + return Problems[i].input_related; } - md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, acme->p, - "Unable to understand ACME server response. Wrong ACME protocol version?"); - rv = APR_EINVAL; - } - else { - md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, acme->p, "unsuccessful in contacting ACME " - "server at %s. If this problem persists, please check your network " - "connectivity from your Apache server to the ACME server. Also, older " - "servers might have trouble verifying the certificates of the ACME " - "server. You can check if you are able to contact it manually via the " - "curl command. Sometimes, the ACME server might be down for maintenance, " - "so failing to contact it is not an immediate problem. mod_md will " - "continue retrying this.", acme->url); } - return rv; + return 0; } /**************************************************************************************************/ @@ -183,26 +120,10 @@ static void req_update_nonce(md_acme_t *acme, apr_table_t *hdrs) } } -static apr_status_t http_update_nonce(const md_http_response_t *res) +static apr_status_t http_update_nonce(const md_http_response_t *res, void *data) { - if (res->headers) { - const char *nonce = apr_table_get(res->headers, "Replay-Nonce"); - if (nonce) { - md_acme_t *acme = res->req->baton; - acme->nonce = apr_pstrdup(acme->p, nonce); - } - } - return res->rv; -} - -static apr_status_t md_acme_new_nonce(md_acme_t *acme) -{ - apr_status_t rv; - long id; - - rv = md_http_HEAD(acme->http, acme->new_reg, NULL, http_update_nonce, acme, &id); - md_http_await(acme->http, id); - return rv; + req_update_nonce(data, res->headers); + return APR_SUCCESS; } static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, const char *url) @@ -215,6 +136,7 @@ static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, co if (rv != APR_SUCCESS) { return NULL; } + apr_pool_tag(pool, "md_acme_req"); req = apr_pcalloc(pool, sizeof(*req)); if (!req) { @@ -226,54 +148,46 @@ static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, co req->p = pool; req->method = method; req->url = url; - req->prot_hdrs = apr_table_make(pool, 5); - if (!req->prot_hdrs) { - apr_pool_destroy(pool); - return NULL; - } + req->prot_fields = md_json_create(pool); req->max_retries = acme->max_retries; - + req->result = md_result_make(req->p, APR_SUCCESS); return req; } -apr_status_t md_acme_req_body_init(md_acme_req_t *req, md_json_t *jpayload) +static apr_status_t acmev2_new_nonce(md_acme_t *acme) { - const char *payload; - size_t payload_len; - - if (!req->acme->acct) { - return APR_EINVAL; - } - - payload = md_json_writep(jpayload, req->p, MD_JSON_FMT_COMPACT); - if (!payload) { - return APR_EINVAL; - } + return md_http_HEAD_perform(acme->http, acme->api.v2.new_nonce, NULL, http_update_nonce, acme); +} - payload_len = strlen(payload); - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->p, - "acct payload(len=%" APR_SIZE_T_FMT "): %s", payload_len, payload); - return md_jws_sign(&req->req_json, req->p, payload, payload_len, - req->prot_hdrs, req->acme->acct_key, NULL); -} +apr_status_t md_acme_init(apr_pool_t *p, const char *base, int init_ssl) +{ + base_product = base; + return init_ssl? md_crypt_init(p) : APR_SUCCESS; +} static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t *res) { const char *ctype; - md_json_t *problem; - + md_json_t *problem = NULL; + apr_status_t rv; + ctype = apr_table_get(req->resp_hdrs, "content-type"); + ctype = md_util_parse_ct(res->req->pool, ctype); if (ctype && !strcmp(ctype, "application/problem+json")) { /* RFC 7807 */ - md_json_read_http(&problem, req->p, res); - if (problem) { + rv = md_json_read_http(&problem, req->p, res); + if (rv == APR_SUCCESS && problem) { const char *ptype, *pdetail; req->resp_json = problem; ptype = md_json_gets(problem, MD_KEY_TYPE, NULL); pdetail = md_json_gets(problem, MD_KEY_DETAIL, NULL); req->rv = problem_status_get(ptype); + md_result_problem_set(req->result, req->rv, ptype, pdetail, + md_json_getj(problem, MD_KEY_SUBPROBLEMS, NULL)); + + if (APR_STATUS_IS_EAGAIN(req->rv)) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, req->rv, req->p, @@ -287,43 +201,79 @@ static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t } } - if (APR_SUCCESS == res->rv) { - switch (res->status) { - case 400: - return APR_EINVAL; - case 403: - return APR_EACCES; - case 404: - return APR_ENOENT; - default: - md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, req->p, - "acme problem unknown: http status %d", res->status); - return APR_EGENERAL; - } + switch (res->status) { + case 400: + return APR_EINVAL; + case 401: /* sectigo returns this instead of 403 */ + case 403: + return APR_EACCES; + case 404: + return APR_ENOENT; + default: + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, req->p, + "acme problem unknown: http status %d", res->status); + md_result_printf(req->result, APR_EGENERAL, "unexpected http status: %d", + res->status); + return req->result->status; } - return res->rv; + return APR_SUCCESS; } /**************************************************************************************************/ /* ACME requests with nonce handling */ -static apr_status_t md_acme_req_done(md_acme_req_t *req) +static apr_status_t acmev2_req_init(md_acme_req_t *req, md_json_t *jpayload) +{ + md_data_t payload; + + md_data_null(&payload); + if (!req->acme->acct) { + return APR_EINVAL; + } + if (jpayload) { + payload.data = md_json_writep(jpayload, req->p, MD_JSON_FMT_COMPACT); + if (!payload.data) { + return APR_EINVAL; + } + } + else { + payload.data = ""; + } + + payload.len = strlen(payload.data); + 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_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) { - apr_status_t rv = req->rv; + return req->acme->req_init_fn(req, payload); +} + +static apr_status_t md_acme_req_done(md_acme_req_t *req, apr_status_t rv) +{ + if (req->result->status != APR_SUCCESS) { + if (req->on_err) { + req->on_err(req, req->result, req->baton); + } + } + /* An error in rv superceeds the result->status */ + if (APR_SUCCESS != rv) req->result->status = rv; + rv = req->result->status; + /* transfer results into the acme's central result for longer life and later inspection */ + md_result_dup(req->acme->last, req->result); if (req->p) { apr_pool_destroy(req->p); } return rv; } -static apr_status_t on_response(const md_http_response_t *res) +static apr_status_t on_response(const md_http_response_t *res, void *data) { - md_acme_req_t *req = res->req->baton; - apr_status_t rv = res->rv; - - if (APR_SUCCESS != rv) { - goto out; - } + md_acme_req_t *req = data; + apr_status_t rv = APR_SUCCESS; req->resp_hdrs = apr_table_clone(req->p, res->headers); req_update_nonce(req->acme, res->headers); @@ -361,9 +311,10 @@ static apr_status_t on_response(const md_http_response_t *res) if (!processed) { rv = APR_EINVAL; - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, req->p, - "response: %d, content-type=%s", res->status, - apr_table_get(res->headers, "Content-Type")); + md_result_printf(req->result, rv, "unable to process the response: " + "http-status=%d, content-type=%s", + res->status, apr_table_get(res->headers, "Content-Type")); + md_result_log(req->result, MD_LOG_ERR); } } else if (APR_EAGAIN == (rv = inspect_problem(req, res))) { @@ -371,85 +322,110 @@ static apr_status_t on_response(const md_http_response_t *res) return rv; } -out: - md_acme_req_done(req); + md_acme_req_done(req, rv); return rv; } +static apr_status_t acmev2_GET_as_POST_init(md_acme_req_t *req, void *baton) +{ + (void)baton; + return md_acme_req_body_init(req, NULL); +} + static apr_status_t md_acme_req_send(md_acme_req_t *req) { apr_status_t rv; md_acme_t *acme = req->acme; - const char *body = NULL; + md_data_t *body = NULL; + md_result_t *result; assert(acme->url); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, req->p, + "sending req: %s %s", req->method, req->url); + md_result_reset(req->acme->last); + result = md_result_make(req->p, APR_SUCCESS); + + /* Whom are we talking to? */ + if (acme->version == MD_ACME_VERSION_UNKNOWN) { + rv = md_acme_setup(acme, result); + if (APR_SUCCESS != rv) goto leave; + } + + if (!strcmp("GET", req->method) && !req->on_init && !req->req_json) { + /* See <https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html#rfc.section.6.3> + * and <https://mailarchive.ietf.org/arch/msg/acme/sotffSQ0OWV-qQJodLwWYWcEVKI> + * and <https://community.letsencrypt.org/t/acme-v2-scheduled-deprecation-of-unauthenticated-resource-gets/74380> + * We implement this change in ACMEv2 and higher as keeping the md_acme_GET() methods, + * but switching them to POSTs with a empty, JWS signed, body when we call + * our HTTP client. */ + req->method = "POST"; + req->on_init = acmev2_GET_as_POST_init; + /*req->max_retries = 0; don't do retries on these "GET"s */ + } + + /* Besides GET/HEAD, we always need a fresh nonce */ if (strcmp("GET", req->method) && strcmp("HEAD", req->method)) { - if (!acme->new_authz) { - if (APR_SUCCESS != (rv = md_acme_setup(acme))) { - return rv; - } + if (acme->version == MD_ACME_VERSION_UNKNOWN) { + rv = md_acme_setup(acme, result); + if (APR_SUCCESS != rv) goto leave; } - if (!acme->nonce) { - if (APR_SUCCESS != (rv = md_acme_new_nonce(acme))) { - md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, req->p, - "error retrieving new nonce from ACME server"); - return rv; - } + if (!acme->nonce && (APR_SUCCESS != (rv = acme->new_nonce_fn(acme)))) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, req->p, + "error retrieving new nonce from ACME server"); + goto leave; } - - apr_table_set(req->prot_hdrs, "nonce", acme->nonce); + + md_json_sets(acme->nonce, req->prot_fields, "nonce", NULL); + md_json_sets(req->url, req->prot_fields, "url", NULL); acme->nonce = NULL; } rv = req->on_init? req->on_init(req, req->baton) : APR_SUCCESS; + if (APR_SUCCESS != rv) goto leave; - if ((rv == APR_SUCCESS) && req->req_json) { - body = md_json_writep(req->req_json, req->p, MD_JSON_FMT_INDENT); - if (!body) { - rv = APR_EINVAL; - } + 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); + body->len = strlen(body->data); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->p, + "sending JSON body: %s", body->data); } - if (rv == APR_SUCCESS) { - long id = 0; - - if (body && md_log_is_level(req->p, MD_LOG_TRACE2)) { - md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, req->p, - "req: POST %s, body:\n%s", req->url, body); - } - else { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, req->p, - "req: POST %s", req->url); - } - if (!strcmp("GET", req->method)) { - rv = md_http_GET(req->acme->http, req->url, NULL, on_response, req, &id); - } - else if (!strcmp("POST", req->method)) { - rv = md_http_POSTd(req->acme->http, req->url, NULL, "application/json", - body, body? strlen(body) : 0, on_response, req, &id); - } - else if (!strcmp("HEAD", req->method)) { - rv = md_http_HEAD(req->acme->http, req->url, NULL, on_response, req, &id); - } - else { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, req->p, - "HTTP method %s against: %s", req->method, req->url); - rv = APR_ENOTIMPL; - } - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, req->p, "req sent"); - md_http_await(acme->http, id); - - if (APR_EAGAIN == rv && req->max_retries > 0) { - --req->max_retries; - return md_acme_req_send(req); - } - req = NULL; + 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); } - - if (req) { - md_acme_req_done(req); + else { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, req->p, + "req: %s %s", req->method, req->url); + } + + if (!strcmp("GET", req->method)) { + rv = md_http_GET_perform(req->acme->http, req->url, NULL, on_response, req); + } + else if (!strcmp("POST", req->method)) { + rv = md_http_POSTd_perform(req->acme->http, req->url, NULL, "application/jose+json", + body, on_response, req); + } + else if (!strcmp("HEAD", req->method)) { + rv = md_http_HEAD_perform(req->acme->http, req->url, NULL, on_response, req); + } + else { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, req->p, + "HTTP method %s against: %s", req->method, req->url); + rv = APR_ENOTIMPL; + } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, req->p, "req sent"); + + if (APR_EAGAIN == rv && req->max_retries > 0) { + --req->max_retries; + rv = md_acme_req_send(req); } + req = NULL; + +leave: + if (req) md_acme_req_done(req, rv); return rv; } @@ -457,6 +433,7 @@ apr_status_t md_acme_POST(md_acme_t *acme, const char *url, md_acme_req_init_cb *on_init, md_acme_req_json_cb *on_json, md_acme_req_res_cb *on_res, + md_acme_req_err_cb *on_err, void *baton) { md_acme_req_t *req; @@ -469,6 +446,7 @@ apr_status_t md_acme_POST(md_acme_t *acme, const char *url, req->on_init = on_init; req->on_json = on_json; req->on_res = on_res; + req->on_err = on_err; req->baton = baton; return md_acme_req_send(req); @@ -478,6 +456,7 @@ apr_status_t md_acme_GET(md_acme_t *acme, const char *url, md_acme_req_init_cb *on_init, md_acme_req_json_cb *on_json, md_acme_req_res_cb *on_res, + md_acme_req_err_cb *on_err, void *baton) { md_acme_req_t *req; @@ -490,11 +469,23 @@ apr_status_t md_acme_GET(md_acme_t *acme, const char *url, req->on_init = on_init; req->on_json = on_json; req->on_res = on_res; + req->on_err = on_err; req->baton = baton; return md_acme_req_send(req); } +void md_acme_report_result(md_acme_t *acme, apr_status_t rv, struct md_result_t *result) +{ + if (acme->last->status == APR_SUCCESS) { + md_result_set(result, rv, NULL); + } + else { + md_result_problem_set(result, acme->last->status, acme->last->problem, + acme->last->detail, acme->last->subproblems); + } +} + /**************************************************************************************************/ /* GET JSON */ @@ -524,8 +515,283 @@ apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme, ctx.pool = p; ctx.json = NULL; - rv = md_acme_GET(acme, url, NULL, on_got_json, NULL, &ctx); + rv = md_acme_GET(acme, url, NULL, on_got_json, NULL, NULL, &ctx); *pjson = (APR_SUCCESS == rv)? ctx.json : NULL; return rv; } +/**************************************************************************************************/ +/* Generic ACME operations */ + +void md_acme_clear_acct(md_acme_t *acme) +{ + acme->acct_id = NULL; + acme->acct = NULL; + acme->acct_key = NULL; +} + +const char *md_acme_acct_id_get(md_acme_t *acme) +{ + return acme->acct_id; +} + +const char *md_acme_acct_url_get(md_acme_t *acme) +{ + return acme->acct? acme->acct->url : NULL; +} + +apr_status_t md_acme_use_acct(md_acme_t *acme, md_store_t *store, + apr_pool_t *p, const char *acct_id) +{ + 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 (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 (md_acme_acct_matches_md(acct, md)) { + 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_save_acct(md_acme_t *acme, apr_pool_t *p, md_store_t *store) +{ + return md_acme_acct_save(store, p, acme, &acme->acct_id, acme->acct, acme->acct_key); +} + +static apr_status_t acmev2_POST_new_account(md_acme_t *acme, + md_acme_req_init_cb *on_init, + md_acme_req_json_cb *on_json, + md_acme_req_res_cb *on_res, + md_acme_req_err_cb *on_err, + void *baton) +{ + return md_acme_POST(acme, acme->api.v2.new_account, on_init, on_json, on_res, on_err, baton); +} + +apr_status_t md_acme_POST_new_account(md_acme_t *acme, + md_acme_req_init_cb *on_init, + md_acme_req_json_cb *on_json, + md_acme_req_res_cb *on_res, + md_acme_req_err_cb *on_err, + void *baton) +{ + return acme->post_new_account_fn(acme, on_init, on_json, on_res, on_err, baton); +} + +/**************************************************************************************************/ +/* ACME setup */ + +apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url, + const char *proxy_url, const char *ca_file) +{ + md_acme_t *acme; + const char *err = NULL; + apr_status_t rv; + apr_uri_t uri_parsed; + size_t len; + + if (!url) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, "create ACME without url"); + return APR_EINVAL; + } + + if (APR_SUCCESS != (rv = md_util_abs_uri_check(p, url, &err))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "invalid ACME uri (%s): %s", err, url); + return rv; + } + + acme = apr_pcalloc(p, sizeof(*acme)); + acme->url = url; + acme->p = p; + acme->user_agent = apr_psprintf(p, "%s mod_md/%s", + base_product, MOD_MD_VERSION); + acme->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL; + acme->max_retries = 99; + acme->ca_file = ca_file; + + if (APR_SUCCESS != (rv = apr_uri_parse(p, url, &uri_parsed))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "parsing ACME uri: %s", url); + return APR_EINVAL; + } + + len = strlen(uri_parsed.hostname); + acme->sname = (len <= 16)? uri_parsed.hostname : apr_pstrdup(p, uri_parsed.hostname + len - 16); + acme->version = MD_ACME_VERSION_UNKNOWN; + acme->last = md_result_make(acme->p, APR_SUCCESS); + + *pacme = acme; + return rv; +} + +typedef struct { + md_acme_t *acme; + md_result_t *result; +} update_dir_ctx; + +static apr_status_t update_directory(const md_http_response_t *res, void *data) +{ + md_http_request_t *req = res->req; + md_acme_t *acme = ((update_dir_ctx *)data)->acme; + md_result_t *result = ((update_dir_ctx *)data)->result; + apr_status_t rv; + md_json_t *json; + const char *s; + + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->pool, "directory lookup response: %d", res->status); + if (res->status == 503) { + md_result_printf(result, APR_EAGAIN, + "The ACME server at <%s> reports that Service is Unavailable (503). This " + "may happen during maintenance for short periods of time.", acme->url); + md_result_log(result, MD_LOG_INFO); + rv = result->status; + goto leave; + } + else if (res->status < 200 || res->status >= 300) { + md_result_printf(result, APR_EAGAIN, + "The ACME server at <%s> responded with HTTP status %d. This " + "is unusual. Please verify that the URL is correct and that you can indeed " + "make request from the server to it by other means, e.g. invoking curl/wget.", + acme->url, res->status); + rv = result->status; + goto leave; + } + + rv = md_json_read_http(&json, req->pool, res); + if (APR_SUCCESS != rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, req->pool, "reading JSON body"); + goto leave; + } + + if (md_log_is_level(acme->p, MD_LOG_TRACE2)) { + s = md_json_writep(json, req->pool, MD_JSON_FMT_INDENT); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, req->pool, + "response: %s", s ? s : "<failed to serialize!>"); + } + + /* What have we got? */ + if ((s = md_json_dups(acme->p, json, "newAccount", NULL))) { + acme->api.v2.new_account = s; + acme->api.v2.new_order = md_json_dups(acme->p, json, "newOrder", NULL); + acme->api.v2.revoke_cert = md_json_dups(acme->p, json, "revokeCert", NULL); + acme->api.v2.key_change = md_json_dups(acme->p, json, "keyChange", NULL); + acme->api.v2.new_nonce = md_json_dups(acme->p, json, "newNonce", NULL); + /* RFC 8555 only requires "directory" and "newNonce" resources. + * mod_md uses "newAccount" and "newOrder" so check for them. + * But mod_md does not use the "revokeCert" or "keyChange" + * resources, so tolerate the absence of those keys. In the + * future if mod_md implements revocation or key rollover then + * the use of those features should be predicated on the + * server's advertised capabilities. */ + if (acme->api.v2.new_account + && acme->api.v2.new_order + && acme->api.v2.new_nonce) { + acme->version = MD_ACME_VERSION_2; + } + 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; + } + else if ((s = md_json_dups(acme->p, json, "new-authz", NULL))) { + acme->api.v1.new_authz = s; + acme->api.v1.new_cert = md_json_dups(acme->p, json, "new-cert", NULL); + acme->api.v1.new_reg = md_json_dups(acme->p, json, "new-reg", NULL); + acme->api.v1.revoke_cert = md_json_dups(acme->p, json, "revoke-cert", NULL); + if (acme->api.v1.new_authz && acme->api.v1.new_cert + && acme->api.v1.new_reg && acme->api.v1.revoke_cert) { + acme->version = MD_ACME_VERSION_1; + } + acme->ca_agreement = md_json_dups(acme->p, json, "meta", "terms-of-service", NULL); + /* we init that far, but will not use the v1 api */ + } + + if (MD_ACME_VERSION_UNKNOWN == acme->version) { + md_result_printf(result, APR_EINVAL, + "Unable to understand ACME server response from <%s>. " + "Wrong ACME protocol version or link?", acme->url); + md_result_log(result, MD_LOG_WARNING); + rv = result->status; + } +leave: + return rv; +} + +apr_status_t md_acme_setup(md_acme_t *acme, md_result_t *result) +{ + apr_status_t rv; + update_dir_ctx ctx; + + assert(acme->url); + acme->version = MD_ACME_VERSION_UNKNOWN; + + if (!acme->http && APR_SUCCESS != (rv = md_http_create(&acme->http, acme->p, + acme->user_agent, acme->proxy_url))) { + return rv; + } + /* TODO: maybe this should be configurable. Let's take some reasonable + * defaults for now that protect our client */ + md_http_set_response_limit(acme->http, 1024*1024); + md_http_set_timeout_default(acme->http, apr_time_from_sec(10 * 60)); + md_http_set_connect_timeout_default(acme->http, apr_time_from_sec(30)); + md_http_set_stalling_default(acme->http, 10, apr_time_from_sec(30)); + md_http_set_ca_file(acme->http, acme->ca_file); + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "get directory from %s", acme->url); + + ctx.acme = acme; + ctx.result = result; + rv = md_http_GET_perform(acme->http, acme->url, NULL, update_directory, &ctx); + + if (APR_SUCCESS != rv && APR_SUCCESS == result->status) { + /* If the result reports no error, we never got a response from the server */ + md_result_printf(result, rv, + "Unsuccessful in contacting ACME server at <%s>. If this problem persists, " + "please check your network connectivity from your Apache server to the " + "ACME server. Also, older servers might have trouble verifying the certificates " + "of the ACME server. You can check if you are able to contact it manually via the " + "curl command. Sometimes, the ACME server might be down for maintenance, " + "so failing to contact it is not an immediate problem. Apache will " + "continue retrying this.", acme->url); + md_result_log(result, MD_LOG_WARNING); + } + return rv; +} + + |