| /* Licensed to the Apache Software Foundation (ASF) under one or more | |
| * contributor license agreements. See the NOTICE file distributed with | |
| * this work for additional information regarding copyright ownership. | |
| * The ASF licenses this file to You under the Apache License, Version 2.0 | |
| * (the "License"); you may not use this file except in compliance with | |
| * the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| #include <assert.h> | |
| #include <stdlib.h> | |
| #include <apr_lib.h> | |
| #include <apr_strings.h> | |
| #include <apr_buckets.h> | |
| #include <apr_hash.h> | |
| #include <apr_uri.h> | |
| #include <httpd.h> | |
| #include "md.h" | |
| #include "md_crypt.h" | |
| #include "md_json.h" | |
| #include "md_jws.h" | |
| #include "md_http.h" | |
| #include "md_log.h" | |
| #include "md_store.h" | |
| #include "md_result.h" | |
| #include "md_util.h" | |
| #include "md_acme.h" | |
| #include "md_acme_acct.h" | |
| static const char *base_product= "-"; | |
| typedef struct acme_problem_status_t acme_problem_status_t; | |
| struct acme_problem_status_t { | |
| 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:agreementRequired", APR_EGENERAL, 1 }, | |
| { "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_EGENERAL, 1 }, | |
| { "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) { | |
| size_t i; | |
| if (ap_strstr_c(type, "urn:ietf:params:") == type) { | |
| type += strlen("urn:ietf:params:"); | |
| } | |
| else if (ap_strstr_c(type, "urn:") == type) { | |
| type += strlen("urn:"); | |
| } | |
| for(i = 0; i < (sizeof(Problems)/sizeof(Problems[0])); ++i) { | |
| if (!apr_cstr_casecmp(type, Problems[i].type)) { | |
| return Problems[i].rv; | |
| } | |
| } | |
| return APR_EGENERAL; | |
| } | |
| int md_acme_problem_is_input_related(const char *problem) { | |
| size_t i; | |
| if (!problem) return 0; | |
| if (ap_strstr_c(problem, "urn:ietf:params:") == problem) { | |
| problem += strlen("urn:ietf:params:"); | |
| } | |
| else if (ap_strstr_c(problem, "urn:") == problem) { | |
| problem += strlen("urn:"); | |
| } | |
| for(i = 0; i < (sizeof(Problems)/sizeof(Problems[0])); ++i) { | |
| if (!apr_cstr_casecmp(problem, Problems[i].type)) { | |
| return Problems[i].input_related; | |
| } | |
| } | |
| return 0; | |
| } | |
| /**************************************************************************************************/ | |
| /* acme requests */ | |
| static void req_update_nonce(md_acme_t *acme, apr_table_t *hdrs) | |
| { | |
| if (hdrs) { | |
| const char *nonce = apr_table_get(hdrs, "Replay-Nonce"); | |
| if (nonce) { | |
| acme->nonce = apr_pstrdup(acme->p, nonce); | |
| } | |
| } | |
| } | |
| static apr_status_t http_update_nonce(const md_http_response_t *res, void *data) | |
| { | |
| 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) | |
| { | |
| apr_pool_t *pool; | |
| md_acme_req_t *req; | |
| apr_status_t rv; | |
| rv = apr_pool_create(&pool, acme->p); | |
| if (rv != APR_SUCCESS) { | |
| return NULL; | |
| } | |
| apr_pool_tag(pool, "md_acme_req"); | |
| req = apr_pcalloc(pool, sizeof(*req)); | |
| if (!req) { | |
| apr_pool_destroy(pool); | |
| return NULL; | |
| } | |
| req->acme = acme; | |
| req->p = pool; | |
| req->method = method; | |
| req->url = url; | |
| req->prot_fields = md_json_create(pool); | |
| req->max_retries = acme->max_retries; | |
| req->result = md_result_make(req->p, APR_SUCCESS); | |
| return req; | |
| } | |
| static apr_status_t acmev2_new_nonce(md_acme_t *acme) | |
| { | |
| return md_http_HEAD_perform(acme->http, acme->api.v2.new_nonce, NULL, http_update_nonce, acme); | |
| } | |
| 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 = 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 */ | |
| 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); | |
| if (ptype) { | |
| req->rv = problem_status_get(ptype); | |
| pdetail = md_json_gets(problem, MD_KEY_DETAIL, NULL); | |
| 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, | |
| "acme reports %s: %s", ptype, pdetail); | |
| } | |
| else { | |
| md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, req->rv, req->p, | |
| "acme problem %s: %s", ptype, pdetail); | |
| } | |
| return req->rv; | |
| } | |
| } | |
| } | |
| switch (res->status) { | |
| case HTTP_BAD_REQUEST: | |
| return APR_EINVAL; | |
| case HTTP_UNAUTHORIZED: /* sectigo returns this instead of 403 */ | |
| case HTTP_FORBIDDEN: | |
| return APR_EACCES; | |
| case HTTP_NOT_FOUND: | |
| 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 APR_SUCCESS; | |
| } | |
| /**************************************************************************************************/ | |
| /* ACME requests with nonce handling */ | |
| 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) | |
| { | |
| 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, void *data) | |
| { | |
| 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); | |
| md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, req->p, "response: %d", res->status); | |
| if (ap_is_HTTP_SUCCESS(res->status)) { | |
| int processed = 0; | |
| if (req->on_json) { | |
| processed = 1; | |
| rv = md_json_read_http(&req->resp_json, req->p, res); | |
| if (APR_SUCCESS == rv) { | |
| if (md_log_is_level(req->p, MD_LOG_TRACE2)) { | |
| const char *s; | |
| s = md_json_writep(req->resp_json, req->p, MD_JSON_FMT_INDENT); | |
| md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, req->p, | |
| "response: %s", | |
| s ? s : "<failed to serialize!>"); | |
| } | |
| rv = req->on_json(req->acme, req->p, req->resp_hdrs, req->resp_json, req->baton); | |
| } | |
| else if (APR_STATUS_IS_ENOENT(rv)) { | |
| /* not JSON content, fall through */ | |
| processed = 0; | |
| } | |
| else { | |
| md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, req->p, "parsing JSON body"); | |
| } | |
| } | |
| if (!processed && req->on_res) { | |
| processed = 1; | |
| rv = req->on_res(req->acme, res, req->baton); | |
| } | |
| if (!processed) { | |
| rv = APR_EINVAL; | |
| 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))) { | |
| /* leave req alive */ | |
| return rv; | |
| } | |
| 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, int get_as_post) | |
| { | |
| apr_status_t rv; | |
| md_acme_t *acme = req->acme; | |
| 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 (get_as_post && !strcmp("GET", req->method) && !req->on_init && !req->req_json) { | |
| /* See <https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html#rfc.section.6.3> | |
| * and <https://mailarchive.ietf.org/arch/msg/acme/sotffSQ0OWV-qQJodLwWYWcEVKI> | |
| * and <https://community.letsencrypt.org/t/acme-v2-scheduled-deprecation-of-unauthenticated-resource-gets/74380> | |
| * 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->version == MD_ACME_VERSION_UNKNOWN) { | |
| rv = md_acme_setup(acme, result); | |
| if (APR_SUCCESS != rv) goto leave; | |
| } | |
| 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; | |
| } | |
| 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 (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 (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_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; | |
| goto leave; | |
| } | |
| /* on_response has already called md_acme_req_done which destroyed | |
| * req and its pool, unless it returned APR_EAGAIN for a retry. */ | |
| md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, "req sent"); | |
| if (APR_EAGAIN == rv && req->max_retries > 0) { | |
| --req->max_retries; | |
| rv = md_acme_req_send(req, 1); | |
| } | |
| req = NULL; | |
| leave: | |
| if (req) md_acme_req_done(req, rv); | |
| return rv; | |
| } | |
| 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; | |
| assert(url); | |
| assert(on_json || on_res); | |
| md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, acme->p, "add acme POST: %s", url); | |
| req = md_acme_req_create(acme, "POST", 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, 1); | |
| } | |
| apr_status_t md_acme_GET(md_acme_t *acme, const char *url, | |
| md_acme_req_init_cb *on_init, | |
| md_acme_req_json_cb *on_json, | |
| md_acme_req_res_cb *on_res, | |
| md_acme_req_err_cb *on_err, | |
| int get_as_post, | |
| void *baton) | |
| { | |
| md_acme_req_t *req; | |
| assert(url); | |
| assert(on_json || on_res); | |
| md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, acme->p, "add acme GET: %s", url); | |
| req = md_acme_req_create(acme, "GET", 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, get_as_post); | |
| } | |
| 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 */ | |
| typedef struct { | |
| apr_pool_t *pool; | |
| md_json_t *json; | |
| } json_ctx; | |
| static apr_status_t on_got_json(md_acme_t *acme, apr_pool_t *p, const apr_table_t *headers, | |
| md_json_t *jbody, void *baton) | |
| { | |
| json_ctx *ctx = baton; | |
| (void)acme; | |
| (void)p; | |
| (void)headers; | |
| ctx->json = md_json_clone(ctx->pool, jbody); | |
| return APR_SUCCESS; | |
| } | |
| apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme, | |
| const char *url, int get_as_post, apr_pool_t *p) | |
| { | |
| apr_status_t rv; | |
| json_ctx ctx; | |
| ctx.pool = p; | |
| ctx.json = NULL; | |
| rv = md_acme_GET(acme, url, NULL, on_got_json, NULL, NULL, get_as_post, &ctx); | |
| *pjson = (APR_SUCCESS == rv)? ctx.json : NULL; | |
| return rv; | |
| } | |
| /**************************************************************************************************/ | |
| /* 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, | |
| const char *proxy_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, AP_SERVER_BASEREVISION); | |
| acme->proxy_url = apr_pstrdup(p, proxy_url); | |
| acme->max_retries = 9; | |
| acme->ca_file = ca_file; | |
| acme->proxy_ca_file = proxy_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 int collect_profiles(void *baton, const char* key, md_json_t *json) | |
| { | |
| update_dir_ctx *ctx = baton; | |
| (void)json; | |
| APR_ARRAY_PUSH(ctx->acme->api.v2.profiles, const char *) = | |
| apr_pstrdup(ctx->acme->p, key); | |
| return 1; | |
| } | |
| 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 == HTTP_SERVICE_UNAVAILABLE) { | |
| 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 < HTTP_OK || res->status >= HTTP_MULTIPLE_CHOICES) { | |
| 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); | |
| acme->api.v2.renewal_info = md_json_dups(acme->p, json, "renewalInfo", NULL); | |
| /* RFC 8555 only requires "directory" and "newNonce" resources. | |
| * mod_md uses "newAccount" and "newOrder" so check for them. | |
| * But mod_md does not use the "revokeCert" or "keyChange" | |
| * 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; | |
| if (md_json_has_key(json, "meta", "profiles", NULL)) { | |
| acme->api.v2.profiles = apr_array_make(acme->p, 5, sizeof(const char*)); | |
| md_json_iterkey(collect_profiles, data, json, "meta", "profiles", NULL); | |
| md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, req->pool, | |
| "found %d profiles in ACME directory meta", | |
| acme->api.v2.profiles->nelts); | |
| } | |
| else { | |
| acme->api.v2.profiles = NULL; | |
| md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, req->pool, | |
| "no profiles in ACME directory meta"); | |
| } | |
| } | |
| 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_http_set_proxy_ca_file(acme->http, acme->proxy_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; | |
| } | |