/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de) * * Licensed 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_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; } md_acme_authz_set_t *md_acme_authz_set_create(apr_pool_t *p, md_acme_t *acme) { md_acme_authz_set_t *authz_set; authz_set = apr_pcalloc(p, sizeof(*authz_set)); authz_set->authzs = apr_array_make(p, 5, sizeof(md_acme_authz_t *)); return authz_set; } md_acme_authz_t *md_acme_authz_set_get(md_acme_authz_set_t *set, const char *domain) { md_acme_authz_t *authz; int i; assert(domain); for (i = 0; i < set->authzs->nelts; ++i) { authz = APR_ARRAY_IDX(set->authzs, i, md_acme_authz_t *); if (!apr_strnatcasecmp(domain, authz->domain)) { return authz; } } return NULL; } apr_status_t md_acme_authz_set_add(md_acme_authz_set_t *set, md_acme_authz_t *authz) { md_acme_authz_t *existing; assert(authz->domain); if (NULL != (existing = md_acme_authz_set_get(set, authz->domain))) { return APR_EINVAL; } APR_ARRAY_PUSH(set->authzs, md_acme_authz_t*) = authz; return APR_SUCCESS; } apr_status_t md_acme_authz_set_remove(md_acme_authz_set_t *set, const char *domain) { md_acme_authz_t *authz; int i; assert(domain); for (i = 0; i < set->authzs->nelts; ++i) { authz = APR_ARRAY_IDX(set->authzs, i, md_acme_authz_t *); if (!apr_strnatcasecmp(domain, authz->domain)) { int n = i + 1; if (n < set->authzs->nelts) { void **elems = (void **)set->authzs->elts; memmove(elems + i, elems + n, (size_t)(set->authzs->nelts - n)); } --set->authzs->nelts; return APR_SUCCESS; } } return APR_ENOENT; } /**************************************************************************************************/ /* 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; } static apr_status_t on_init_authz(md_acme_req_t *req, void *baton) { authz_req_ctx *ctx = baton; md_json_t *jpayload; jpayload = md_json_create(req->p); md_json_sets("new-authz", jpayload, MD_KEY_RESOURCE, NULL); md_json_sets("dns", jpayload, MD_KEY_IDENTIFIER, MD_KEY_TYPE, NULL); md_json_sets(ctx->domain, jpayload, MD_KEY_IDENTIFIER, MD_KEY_VALUE, NULL); return md_acme_req_body_init(req, jpayload); } static apr_status_t authz_created(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, md_json_t *body, void *baton) { authz_req_ctx *ctx = baton; const char *location = apr_table_get(hdrs, "location"); apr_status_t rv = APR_SUCCESS; if (location) { ctx->authz = md_acme_authz_create(ctx->p); ctx->authz->domain = apr_pstrdup(ctx->p, ctx->domain); ctx->authz->location = apr_pstrdup(ctx->p, location); ctx->authz->resource = md_json_clone(ctx->p, body); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ctx->p, "authz_new at %s", location); } else { rv = APR_EINVAL; md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, ctx->p, "new authz, no location header"); } return rv; } apr_status_t md_acme_authz_register(struct md_acme_authz_t **pauthz, md_acme_t *acme, md_store_t *store, const char *domain, apr_pool_t *p) { apr_status_t rv; authz_req_ctx ctx; authz_req_ctx_init(&ctx, acme, domain, NULL, p); md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "create new authz"); rv = md_acme_POST(acme, acme->new_authz, on_init_authz, authz_created, NULL, &ctx); *pauthz = (APR_SUCCESS == rv)? ctx.authz : NULL; return rv; } /**************************************************************************************************/ /* Update an exiosting authorization */ apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store, apr_pool_t *p) { md_json_t *json; const char *s; apr_status_t rv; assert(acme); assert(acme->http); assert(authz); assert(authz->location); if (APR_SUCCESS != (rv = md_acme_get_json(&json, acme, authz->location, p))) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "update authz for %s at %s", authz->domain, authz->location); return rv; } authz->resource = json; s = md_json_gets(json, "identifier", "type", NULL); if (!s || strcmp(s, "dns")) return APR_EINVAL; s = md_json_gets(json, "identifier", "value", NULL); if (!s || strcmp(s, authz->domain)) return APR_EINVAL; authz->state = MD_ACME_AUTHZ_S_UNKNOWN; s = md_json_gets(json, "status", NULL); if (s && !strcmp(s, "pending")) { authz->state = MD_ACME_AUTHZ_S_PENDING; } else if (s && !strcmp(s, "valid")) { authz->state = MD_ACME_AUTHZ_S_VALID; } else if (s && !strcmp(s, "invalid")) { authz->state = MD_ACME_AUTHZ_S_INVALID; } else if (s) { md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "unknown authz state '%s' " "for %s in %s", s, authz->domain, authz->location); return APR_EINVAL; } 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); 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) { authz_req_ctx *ctx = baton; md_json_t *jpayload; jpayload = md_json_create(req->p); md_json_sets("challenge", jpayload, MD_KEY_RESOURCE, NULL); md_json_sets(ctx->challenge->key_authz, jpayload, MD_KEY_KEYAUTHZ, NULL); 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; md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "updated authz %s", ctx->authz->location); 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; 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_pkey_spec_t *key_spec, apr_pool_t *p) { const char *data; apr_status_t rv; int notify_server; 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)) { rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, MD_FN_HTTP01, MD_SV_TEXT, (void*)cha->key_authz, 0); authz->dir = authz->domain; notify_server = 1; } if (APR_SUCCESS == rv && notify_server) { authz_req_ctx ctx; /* 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, &ctx); } out: return rv; } static apr_status_t setup_cha_dns(const char **pdns, md_acme_authz_cha_t *cha, apr_pool_t *p) { const char *dhex; char *dns; apr_size_t dhex_len; apr_status_t rv; rv = md_crypt_sha256_digest_hex(&dhex, p, cha->key_authz, strlen(cha->key_authz)); if (APR_SUCCESS == rv) { dhex = md_util_str_tolower((char*)dhex); dhex_len = strlen(dhex); assert(dhex_len > 32); dns = apr_pcalloc(p, dhex_len + 1 + sizeof(MD_TLSSNI01_DNS_SUFFIX)); strncpy(dns, dhex, 32); dns[32] = '.'; strncpy(dns+33, dhex+32, dhex_len-32); memcpy(dns+(dhex_len+1), MD_TLSSNI01_DNS_SUFFIX, sizeof(MD_TLSSNI01_DNS_SUFFIX)); } *pdns = (APR_SUCCESS == rv)? dns : NULL; return rv; } static apr_status_t cha_tls_sni_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store, md_pkey_spec_t *key_spec, apr_pool_t *p) { md_cert_t *cha_cert; md_pkey_t *cha_key; const char *cha_dns; apr_status_t rv; int notify_server; if ( APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, ¬ify_server)) || APR_SUCCESS != (rv = setup_cha_dns(&cha_dns, cha, p))) { goto out; } rv = md_store_load(store, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_CERT, MD_SV_CERT, (void**)&cha_cert, p); if ((APR_SUCCESS == rv && !md_cert_covers_domain(cha_cert, cha_dns)) || 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-sni-01 challgenge key", authz->domain); goto out; } /* setup a certificate containing the challenge dns */ rv = md_cert_self_sign(&cha_cert, authz->domain, cha_dns, cha_key, apr_time_from_sec(7 * MD_SECS_PER_DAY), p); if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: setup self signed cert for %s", authz->domain, cha_dns); goto out; } rv = md_store_save(store, p, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_PKEY, MD_SV_PKEY, (void*)cha_key, 0); if (APR_SUCCESS == rv) { rv = md_store_save(store, p, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_CERT, MD_SV_CERT, (void*)cha_cert, 0); } authz->dir = cha_dns; notify_server = 1; } if (APR_SUCCESS == rv && notify_server) { authz_req_ctx ctx; /* 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, &ctx); } out: return rv; } typedef apr_status_t cha_starter(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store, md_pkey_spec_t *key_spec, apr_pool_t *p); typedef struct { const char *name; cha_starter *start; } cha_type; static const cha_type CHA_TYPES[] = { { MD_AUTHZ_TYPE_HTTP01, cha_http_01_setup }, { MD_AUTHZ_TYPE_TLSSNI01, cha_tls_sni_01_setup }, }; 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 = md_json_gets(json, MD_KEY_TYPE, NULL); if (ctype) { 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_pkey_spec_t *key_spec, apr_pool_t *p) { apr_status_t rv; int i; cha_find_ctx fctx; assert(acme); assert(authz); assert(authz->resource); fctx.p = p; fctx.accepted = NULL; /* Look in the order challenge types are defined */ for (i = 0; i < challenges->nelts && !fctx.accepted; ++i) { fctx.type = APR_ARRAY_IDX(challenges, i, const char *); md_json_itera(find_type, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL); } if (!fctx.accepted) { 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_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "%s: the server offers no ACME challenge that is configured " "for this MD. The server offered '%s' and available for this " "MD are: '%s' (via %s).", authz->domain, apr_array_pstrcat(p, fctx.offered, ' '), apr_array_pstrcat(p, challenges, ' '), authz->location); return rv; } for (i = 0; i < CHA_TYPES_LEN; ++i) { if (!apr_strnatcasecmp(CHA_TYPES[i].name, fctx.accepted->type)) { return CHA_TYPES[i].start(fctx.accepted, authz, acme, store, key_spec, p); } } rv = APR_ENOTIMPL; md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: no implementation found for challenge '%s'", authz->domain, fctx.accepted->type); return rv; } /**************************************************************************************************/ /* Delete an existing authz resource */ typedef struct { apr_pool_t *p; md_acme_authz_t *authz; } del_ctx; static apr_status_t on_init_authz_del(md_acme_req_t *req, void *baton) { md_json_t *jpayload; jpayload = md_json_create(req->p); md_json_sets("deactivated", jpayload, MD_KEY_STATUS, NULL); return md_acme_req_body_init(req, jpayload); } static apr_status_t authz_del(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, md_json_t *body, void *baton) { authz_req_ctx *ctx = baton; md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "deleted authz %s", ctx->authz->location); acme->acct = NULL; return APR_SUCCESS; } apr_status_t md_acme_authz_del(md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store, apr_pool_t *p) { authz_req_ctx ctx; ctx.p = p; ctx.authz = authz; md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "delete authz for %s from %s", authz->domain, authz->location); return md_acme_POST(acme, authz->location, on_init_authz_del, authz_del, NULL, &ctx); } /**************************************************************************************************/ /* authz conversion */ md_json_t *md_acme_authz_to_json(md_acme_authz_t *a, apr_pool_t *p) { md_json_t *json = md_json_create(p); if (json) { md_json_sets(a->domain, json, MD_KEY_DOMAIN, NULL); md_json_sets(a->location, json, MD_KEY_LOCATION, NULL); md_json_sets(a->dir, json, MD_KEY_DIR, NULL); md_json_setl(a->state, json, MD_KEY_STATE, NULL); return json; } return NULL; } md_acme_authz_t *md_acme_authz_from_json(struct md_json_t *json, apr_pool_t *p) { md_acme_authz_t *authz = md_acme_authz_create(p); if (authz) { authz->domain = md_json_dups(p, json, MD_KEY_DOMAIN, NULL); authz->location = md_json_dups(p, json, MD_KEY_LOCATION, NULL); authz->dir = md_json_dups(p, json, MD_KEY_DIR, NULL); authz->state = (md_acme_authz_state_t)md_json_getl(json, MD_KEY_STATE, NULL); return authz; } return NULL; } /**************************************************************************************************/ /* authz_set conversion */ #define MD_KEY_ACCOUNT "account" #define MD_KEY_AUTHZS "authorizations" static apr_status_t authz_to_json(void *value, md_json_t *json, apr_pool_t *p, void *baton) { return md_json_setj(md_acme_authz_to_json(value, p), json, NULL); } static apr_status_t authz_from_json(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton) { *pvalue = md_acme_authz_from_json(json, p); return (*pvalue)? APR_SUCCESS : APR_EINVAL; } md_json_t *md_acme_authz_set_to_json(md_acme_authz_set_t *set, apr_pool_t *p) { md_json_t *json = md_json_create(p); if (json) { md_json_seta(set->authzs, authz_to_json, NULL, json, MD_KEY_AUTHZS, NULL); return json; } return NULL; } md_acme_authz_set_t *md_acme_authz_set_from_json(md_json_t *json, apr_pool_t *p) { md_acme_authz_set_t *set = md_acme_authz_set_create(p, NULL); if (set) { md_json_geta(set->authzs, authz_from_json, NULL, json, MD_KEY_AUTHZS, NULL); return set; } return NULL; } /**************************************************************************************************/ /* persistence */ apr_status_t md_acme_authz_set_load(struct md_store_t *store, md_store_group_t group, const char *md_name, md_acme_authz_set_t **pauthz_set, apr_pool_t *p) { apr_status_t rv; md_json_t *json; md_acme_authz_set_t *authz_set; rv = md_store_load_json(store, group, md_name, MD_FN_AUTHZ, &json, p); if (APR_SUCCESS == rv) { authz_set = md_acme_authz_set_from_json(json, p); } *pauthz_set = (APR_SUCCESS == rv)? authz_set : NULL; return rv; } static apr_status_t p_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) { md_store_t *store = baton; md_json_t *json; md_store_group_t group; md_acme_authz_set_t *set; const char *md_name; int create; group = (md_store_group_t)va_arg(ap, int); md_name = va_arg(ap, const char *); set = va_arg(ap, md_acme_authz_set_t *); create = va_arg(ap, int); json = md_acme_authz_set_to_json(set, ptemp); assert(json); return md_store_save_json(store, ptemp, group, md_name, MD_FN_AUTHZ, json, create); } apr_status_t md_acme_authz_set_save(struct md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *md_name, md_acme_authz_set_t *authz_set, int create) { return md_util_pool_vdo(p_save, store, p, group, md_name, authz_set, create, NULL); } static apr_status_t p_purge(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) { md_store_t *store = baton; md_acme_authz_set_t *authz_set; const md_acme_authz_t *authz; md_store_group_t group; const char *md_name; int i; group = (md_store_group_t)va_arg(ap, int); md_name = va_arg(ap, const char *); if (APR_SUCCESS == md_acme_authz_set_load(store, group, md_name, &authz_set, p)) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz_set loaded for %s", md_name); for (i = 0; i < authz_set->authzs->nelts; ++i) { authz = APR_ARRAY_IDX(authz_set->authzs, i, const md_acme_authz_t*); md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz check %s", authz->domain); if (authz->dir) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz purge %s", authz->dir); md_store_purge(store, p, MD_SG_CHALLENGES, authz->dir); } } } return md_store_remove(store, group, md_name, MD_FN_AUTHZ, ptemp, 1); } apr_status_t md_acme_authz_set_purge(md_store_t *store, md_store_group_t group, apr_pool_t *p, const char *md_name) { return md_util_pool_vdo(p_purge, store, p, group, md_name, NULL); }