/* 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 #include #include #include #include #include #include #include #include #include #include "md.h" #include "md_crypt.h" #include "md_json.h" #include "md_http.h" #include "md_log.h" #include "md_jws.h" #include "md_result.h" #include "md_store.h" #include "md_util.h" #include "md_acme.h" #include "md_acme_authz.h" md_acme_authz_t *md_acme_authz_create(apr_pool_t *p) { md_acme_authz_t *authz; authz = apr_pcalloc(p, sizeof(*authz)); return authz; } /**************************************************************************************************/ /* Register a new authorization */ typedef struct { size_t index; const char *type; const char *uri; const char *token; const char *key_authz; } md_acme_authz_cha_t; typedef struct { apr_pool_t *p; md_acme_t *acme; const char *domain; md_acme_authz_t *authz; md_acme_authz_cha_t *challenge; } authz_req_ctx; static void authz_req_ctx_init(authz_req_ctx *ctx, md_acme_t *acme, const char *domain, md_acme_authz_t *authz, apr_pool_t *p) { memset(ctx, 0, sizeof(*ctx)); ctx->p = p; ctx->acme = acme; ctx->domain = domain; ctx->authz = authz; } /**************************************************************************************************/ /* Update an existing authorization */ apr_status_t md_acme_authz_retrieve(md_acme_t *acme, apr_pool_t *p, const char *url, md_acme_authz_t **pauthz) { md_acme_authz_t *authz; apr_status_t rv; authz = apr_pcalloc(p, sizeof(*authz)); authz->url = apr_pstrdup(p, url); rv = md_acme_authz_update(authz, acme, p); *pauthz = (APR_SUCCESS == rv)? authz : NULL; return rv; } typedef struct { apr_pool_t *p; md_acme_authz_t *authz; } error_ctx_t; static int copy_challenge_error(void *baton, size_t index, md_json_t *json) { error_ctx_t *ctx = baton; (void)index; if (md_json_has_key(json, MD_KEY_ERROR, NULL)) { ctx->authz->error_type = md_json_dups(ctx->p, json, MD_KEY_ERROR, MD_KEY_TYPE, NULL); ctx->authz->error_detail = md_json_dups(ctx->p, json, MD_KEY_ERROR, MD_KEY_DETAIL, NULL); ctx->authz->error_subproblems = md_json_dupj(ctx->p, json, MD_KEY_ERROR, MD_KEY_SUBPROBLEMS, NULL); } return 1; } apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, apr_pool_t *p) { md_json_t *json; const char *s, *err; md_log_level_t log_level; apr_status_t rv; error_ctx_t ctx; assert(acme); assert(acme->http); assert(authz); assert(authz->url); authz->state = MD_ACME_AUTHZ_S_UNKNOWN; json = NULL; authz->error_type = authz->error_detail = NULL; authz->error_subproblems = NULL; err = "unable to parse response"; log_level = MD_LOG_ERR; if (APR_SUCCESS == (rv = md_acme_get_json(&json, acme, authz->url, p)) && (s = md_json_gets(json, MD_KEY_STATUS, NULL))) { authz->domain = md_json_gets(json, MD_KEY_IDENTIFIER, MD_KEY_VALUE, NULL); authz->resource = json; if (!strcmp(s, "pending")) { authz->state = MD_ACME_AUTHZ_S_PENDING; err = "challenge 'pending'"; log_level = MD_LOG_DEBUG; } else if (!strcmp(s, "valid")) { authz->state = MD_ACME_AUTHZ_S_VALID; err = "challenge 'valid'"; log_level = MD_LOG_DEBUG; } else if (!strcmp(s, "invalid")) { ctx.p = p; ctx.authz = authz; authz->state = MD_ACME_AUTHZ_S_INVALID; md_json_itera(copy_challenge_error, &ctx, json, MD_KEY_CHALLENGES, NULL); err = "challenge 'invalid'"; } } if (json && authz->state == MD_ACME_AUTHZ_S_UNKNOWN) { err = "unable to understand response"; rv = APR_EINVAL; } if (md_log_is_level(p, log_level)) { md_log_perror(MD_LOG_MARK, log_level, rv, p, "ACME server authz: %s for %s at %s. " "Exact response was: %s", err, authz->domain, authz->url, json? md_json_writep(json, p, MD_JSON_FMT_COMPACT) : "not available"); } return rv; } /**************************************************************************************************/ /* response to a challenge */ static md_acme_authz_cha_t *cha_from_json(apr_pool_t *p, size_t index, md_json_t *json) { md_acme_authz_cha_t * cha; cha = apr_pcalloc(p, sizeof(*cha)); cha->index = index; cha->type = md_json_dups(p, json, MD_KEY_TYPE, NULL); if (md_json_has_key(json, MD_KEY_URL, NULL)) { /* ACMEv2 */ cha->uri = md_json_dups(p, json, MD_KEY_URL, NULL); } else { /* ACMEv1 */ cha->uri = md_json_dups(p, json, MD_KEY_URI, NULL); } cha->token = md_json_dups(p, json, MD_KEY_TOKEN, NULL); cha->key_authz = md_json_dups(p, json, MD_KEY_KEYAUTHZ, NULL); return cha; } static apr_status_t on_init_authz_resp(md_acme_req_t *req, void *baton) { md_json_t *jpayload; (void)baton; jpayload = md_json_create(req->p); return md_acme_req_body_init(req, jpayload); } static apr_status_t authz_http_set(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, md_json_t *body, void *baton) { authz_req_ctx *ctx = baton; (void)acme; (void)p; (void)hdrs; (void)body; md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ctx->p, "updated authz %s", ctx->authz->url); return APR_SUCCESS; } static apr_status_t setup_key_authz(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, md_acme_t *acme, apr_pool_t *p, int *pchanged) { const char *thumb64, *key_authz; apr_status_t rv; (void)authz; assert(cha); assert(cha->token); *pchanged = 0; if (APR_SUCCESS == (rv = md_jws_pkey_thumb(&thumb64, p, acme->acct_key))) { key_authz = apr_psprintf(p, "%s.%s", cha->token, thumb64); if (cha->key_authz) { if (strcmp(key_authz, cha->key_authz)) { /* Hu? Did the account change key? */ cha->key_authz = NULL; } } if (!cha->key_authz) { cha->key_authz = key_authz; *pchanged = 1; } } return rv; } static apr_status_t cha_http_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store, md_pkeys_spec_t *key_specs, apr_array_header_t *acme_tls_1_domains, const md_t *md, apr_table_t *env, md_result_t *result, apr_pool_t *p) { const char *data; apr_status_t rv; int notify_server; (void)key_specs; (void)env; (void)acme_tls_1_domains; (void)md; if (APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, ¬ify_server))) { goto out; } rv = md_store_load(store, MD_SG_CHALLENGES, authz->domain, MD_FN_HTTP01, MD_SV_TEXT, (void**)&data, p); if ((APR_SUCCESS == rv && strcmp(cha->key_authz, data)) || APR_STATUS_IS_ENOENT(rv)) { const char *content = apr_psprintf(p, "%s\n", cha->key_authz); rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, MD_FN_HTTP01, MD_SV_TEXT, (void*)content, 0); notify_server = 1; } if (APR_SUCCESS == rv && notify_server) { authz_req_ctx ctx; const char *event; /* Raise event that challenge data has been set up before we tell the ACME server. Clusters might want to distribute it. */ event = apr_psprintf(p, "challenge-setup:%s:%s", MD_AUTHZ_TYPE_HTTP01, authz->domain); rv = md_result_raise(result, event, p); if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: event '%s' failed. aborting challenge setup", authz->domain, event); goto out; } /* challenge is setup or was changed from previous data, tell ACME server * so it may (re)try verification */ authz_req_ctx_init(&ctx, acme, NULL, authz, p); ctx.challenge = cha; rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, NULL, &ctx); } out: return rv; } void tls_alpn01_fnames(apr_pool_t *p, md_pkey_spec_t *kspec, char **keyfn, char **certfn ) { *keyfn = apr_pstrcat(p, "acme-tls-alpn-01-", md_pkey_filename(kspec, p), NULL); *certfn = apr_pstrcat(p, "acme-tls-alpn-01-", md_chain_filename(kspec, p), NULL); } static apr_status_t cha_tls_alpn_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store, md_pkeys_spec_t *key_specs, apr_array_header_t *acme_tls_1_domains, const md_t *md, apr_table_t *env, md_result_t *result, apr_pool_t *p) { const char *acme_id, *token; apr_status_t rv; int notify_server; md_data_t data; int i; (void)env; (void)md; if (md_array_str_index(acme_tls_1_domains, authz->domain, 0, 0) < 0) { rv = APR_ENOTIMPL; if (acme_tls_1_domains->nelts) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "%s: protocol 'acme-tls/1' seems not enabled for this domain, " "but is enabled for other associated domains. " "Continuing with fingers crossed.", authz->domain); } else { md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, "%s: protocol 'acme-tls/1' seems not enabled for this or " "any other associated domain. Not attempting challenge " "type tls-alpn-01.", authz->domain); goto out; } } if (APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, ¬ify_server))) { goto out; } /* Create a "tls-alpn-01" certificate for the domain we want to authenticate. * The server will need to answer a TLS connection with SNI == authz->domain * and ALPN protocol "acme-tls/1" with this certificate. */ md_data_init_str(&data, cha->key_authz); rv = md_crypt_sha256_digest_hex(&token, p, &data); if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create tls-alpn-01 validation token", authz->domain); goto out; } acme_id = apr_psprintf(p, "critical,DER:04:20:%s", token); /* Each configured key type must be generated to ensure: * that any fallback certs already given to mod_ssl are replaced. * We expect that the validation client (at the CA) can deal with at * least one of them. */ for (i = 0; i < md_pkeys_spec_count(key_specs); ++i) { char *kfn, *cfn; md_cert_t *cha_cert; md_pkey_t *cha_key; md_pkey_spec_t *key_spec; key_spec = md_pkeys_spec_get(key_specs, i); tls_alpn01_fnames(p, key_spec, &kfn, &cfn); rv = md_store_load(store, MD_SG_CHALLENGES, authz->domain, cfn, MD_SV_CERT, (void**)&cha_cert, p); if ((APR_SUCCESS == rv && !md_cert_covers_domain(cha_cert, authz->domain)) || APR_STATUS_IS_ENOENT(rv)) { if (APR_SUCCESS != (rv = md_pkey_gen(&cha_key, p, key_spec))) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create tls-alpn-01 %s challenge key", authz->domain, md_pkey_spec_name(key_spec)); goto out; } if (APR_SUCCESS != (rv = md_cert_make_tls_alpn_01(&cha_cert, authz->domain, acme_id, cha_key, apr_time_from_sec(7 * MD_SECS_PER_DAY), p))) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create tls-alpn-01 %s challenge cert", authz->domain, md_pkey_spec_name(key_spec)); goto out; } if (APR_SUCCESS == (rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, kfn, MD_SV_PKEY, (void*)cha_key, 0))) { rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, cfn, MD_SV_CERT, (void*)cha_cert, 0); } ++notify_server; } } if (APR_SUCCESS == rv && notify_server) { authz_req_ctx ctx; const char *event; /* Raise event that challenge data has been set up before we tell the ACME server. Clusters might want to distribute it. */ event = apr_psprintf(p, "challenge-setup:%s:%s", MD_AUTHZ_TYPE_TLSALPN01, authz->domain); rv = md_result_raise(result, event, p); if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: event '%s' failed. aborting challenge setup", authz->domain, event); goto out; } /* challenge is setup or was changed from previous data, tell ACME server * so it may (re)try verification */ authz_req_ctx_init(&ctx, acme, NULL, authz, p); ctx.challenge = cha; rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, NULL, &ctx); } out: return rv; } static apr_status_t cha_dns_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store, md_pkeys_spec_t *key_specs, apr_array_header_t *acme_tls_1_domains, const md_t *md, apr_table_t *env, md_result_t *result, apr_pool_t *p) { const char *token; const char * const *argv; const char *cmdline, *dns01_cmd; apr_status_t rv; int exit_code, notify_server; authz_req_ctx ctx; md_data_t data; const char *event; (void)store; (void)key_specs; (void)acme_tls_1_domains; dns01_cmd = md->dns01_cmd; if (!dns01_cmd) dns01_cmd = apr_table_get(env, MD_KEY_CMD_DNS01); if (!dns01_cmd) { rv = APR_ENOTIMPL; md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: dns-01 command not set", authz->domain); goto out; } if (APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, ¬ify_server))) { goto out; } md_data_init_str(&data, cha->key_authz); rv = md_crypt_sha256_digest64(&token, p, &data); if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create dns-01 token for %s", md->name, authz->domain); goto out; } cmdline = apr_psprintf(p, "%s setup %s %s", dns01_cmd, authz->domain, token); md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "%s: dns-01 setup command: %s", authz->domain, cmdline); apr_tokenize_to_argv(cmdline, (char***)&argv, p); if (APR_SUCCESS != (rv = md_util_exec(p, argv[0], argv, NULL, &exit_code))) { md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "%s: dns-01 setup command failed to execute for %s", md->name, authz->domain); goto out; } if (exit_code) { rv = APR_EGENERAL; md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, "%s: dns-01 setup command returns %d for %s", md->name, exit_code, authz->domain); goto out; } /* Raise event that challenge data has been set up before we tell the ACME server. Clusters might want to distribute it. */ event = apr_psprintf(p, "challenge-setup:%s:%s", MD_AUTHZ_TYPE_DNS01, authz->domain); rv = md_result_raise(result, event, p); if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: event '%s' failed. aborting challenge setup", authz->domain, event); goto out; } /* challenge is setup, tell ACME server so it may (re)try verification */ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: dns-01 setup succeeded for %s", md->name, authz->domain); authz_req_ctx_init(&ctx, acme, NULL, authz, p); ctx.challenge = cha; rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, NULL, &ctx); out: return rv; } static apr_status_t cha_dns_01_teardown(md_store_t *store, const char *domain, const md_t *md, apr_table_t *env, apr_pool_t *p) { const char * const *argv; const char *cmdline, *dns01_cmd; apr_status_t rv; int exit_code; (void)store; dns01_cmd = md->dns01_cmd; if (!dns01_cmd) dns01_cmd = apr_table_get(env, MD_KEY_CMD_DNS01); if (!dns01_cmd) { rv = APR_ENOTIMPL; md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "%s: dns-01 command not set for %s", md->name, domain); goto out; } cmdline = apr_psprintf(p, "%s teardown %s", dns01_cmd, domain); apr_tokenize_to_argv(cmdline, (char***)&argv, p); if (APR_SUCCESS != (rv = md_util_exec(p, argv[0], argv, NULL, &exit_code)) || exit_code) { md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "%s: dns-01 teardown command failed (exit code=%d) for %s", md->name, exit_code, domain); } out: return rv; } static apr_status_t cha_teardown_dir(md_store_t *store, const char *domain, const md_t *md, apr_table_t *env, apr_pool_t *p) { (void)md; (void)env; return md_store_purge(store, p, MD_SG_CHALLENGES, domain); } typedef apr_status_t cha_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store, md_pkeys_spec_t *key_specs, apr_array_header_t *acme_tls_1_domains, const md_t *md, apr_table_t *env, md_result_t *result, apr_pool_t *p); typedef apr_status_t cha_teardown(md_store_t *store, const char *domain, const md_t *md, apr_table_t *env, apr_pool_t *p); typedef struct { const char *name; cha_setup *setup; cha_teardown *teardown; } cha_type; static const cha_type CHA_TYPES[] = { { MD_AUTHZ_TYPE_HTTP01, cha_http_01_setup, cha_teardown_dir }, { MD_AUTHZ_TYPE_TLSALPN01, cha_tls_alpn_01_setup, cha_teardown_dir }, { MD_AUTHZ_TYPE_DNS01, cha_dns_01_setup, cha_dns_01_teardown }, }; static const apr_size_t CHA_TYPES_LEN = (sizeof(CHA_TYPES)/sizeof(CHA_TYPES[0])); typedef struct { apr_pool_t *p; const char *type; md_acme_authz_cha_t *accepted; apr_array_header_t *offered; } cha_find_ctx; static apr_status_t collect_offered(void *baton, size_t index, md_json_t *json) { cha_find_ctx *ctx = baton; const char *ctype; (void)index; if ((ctype = md_json_gets(json, MD_KEY_TYPE, NULL))) { APR_ARRAY_PUSH(ctx->offered, const char*) = apr_pstrdup(ctx->p, ctype); } return 1; } static apr_status_t find_type(void *baton, size_t index, md_json_t *json) { cha_find_ctx *ctx = baton; const char *ctype = md_json_gets(json, MD_KEY_TYPE, NULL); if (ctype && !apr_strnatcasecmp(ctx->type, ctype)) { ctx->accepted = cha_from_json(ctx->p, index, json); return 0; } return 1; } apr_status_t md_acme_authz_respond(md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store, apr_array_header_t *challenges, md_pkeys_spec_t *key_specs, apr_array_header_t *acme_tls_1_domains, const md_t *md, apr_table_t *env, apr_pool_t *p, const char **psetup_token, md_result_t *result) { apr_status_t rv; int i, j; cha_find_ctx fctx; const char *challenge_setup; assert(acme); assert(authz); assert(authz->resource); fctx.p = p; fctx.accepted = NULL; /* Look in the order challenge types are defined: * - if they are offered by the CA, try to set it up * - if setup was successful, we are done and the CA will evaluate us * - if setup failed, continue to look for another supported challenge type * - if there is no overlap in types, tell the user that she has to configure * either more types (dns, tls-alpn-01), make ports available or refrain * from using wildcard domains when dns is not available. etc. * - if there was an overlap, but no setup was successful, report that. We * will retry this, maybe the failure is temporary (e.g. command to setup DNS */ md_result_printf(result, 0, "%s: selecting suitable authorization challenge " "type, this domain supports %s", authz->domain, apr_array_pstrcat(p, challenges, ' ')); rv = APR_ENOTIMPL; challenge_setup = NULL; for (i = 0; i < challenges->nelts; ++i) { fctx.type = APR_ARRAY_IDX(challenges, i, const char *); fctx.accepted = NULL; md_json_itera(find_type, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "%s: challenge type '%s' for %s: %s", authz->domain, fctx.type, md->name, fctx.accepted? "maybe acceptable" : "not applicable"); if (fctx.accepted) { for (j = 0; j < (int)CHA_TYPES_LEN; ++j) { if (!apr_strnatcasecmp(CHA_TYPES[j].name, fctx.accepted->type)) { md_result_activity_printf(result, "Setting up challenge '%s' for domain %s", fctx.accepted->type, authz->domain); rv = CHA_TYPES[j].setup(fctx.accepted, authz, acme, store, key_specs, acme_tls_1_domains, md, env, result, p); if (APR_SUCCESS == rv) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: set up challenge '%s' for %s", authz->domain, fctx.accepted->type, md->name); challenge_setup = CHA_TYPES[j].name; goto out; } md_result_printf(result, rv, "error setting up challenge '%s' for %s, " "for domain %s, looking for other option", fctx.accepted->type, authz->domain, md->name); md_result_log(result, MD_LOG_INFO); } } } } out: *psetup_token = (APR_SUCCESS == rv)? apr_psprintf(p, "%s:%s", challenge_setup, authz->domain) : NULL; if (!fctx.accepted || APR_ENOTIMPL == rv) { rv = APR_EINVAL; fctx.offered = apr_array_make(p, 5, sizeof(const char*)); md_json_itera(collect_offered, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL); md_result_printf(result, rv, "None of offered challenge types for domain %s are supported. " "The server offered '%s' and available are: '%s'.", authz->domain, apr_array_pstrcat(p, fctx.offered, ' '), apr_array_pstrcat(p, challenges, ' ')); result->problem = "challenge-mismatch"; md_result_log(result, MD_LOG_ERR); } else if (APR_SUCCESS != rv) { fctx.offered = apr_array_make(p, 5, sizeof(const char*)); md_json_itera(collect_offered, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL); md_result_printf(result, rv, "None of the offered challenge types %s offered " "for domain %s could be setup successfully. Please check the " "log for errors.", authz->domain, apr_array_pstrcat(p, fctx.offered, ' ')); result->problem = "challenge-setup-failure"; md_result_log(result, MD_LOG_ERR); } return rv; } apr_status_t md_acme_authz_teardown(struct md_store_t *store, const char *token, const md_t *md, apr_table_t *env, apr_pool_t *p) { char *challenge, *domain; int i; if (strchr(token, ':')) { challenge = apr_pstrdup(p, token); domain = strchr(challenge, ':'); *domain = '\0'; domain++; for (i = 0; i < (int)CHA_TYPES_LEN; ++i) { if (!apr_strnatcasecmp(CHA_TYPES[i].name, challenge)) { if (CHA_TYPES[i].teardown) { return CHA_TYPES[i].teardown(store, domain, md, env, p); } break; } } } return APR_SUCCESS; }