diff options
Diffstat (limited to 'modules/md/md_acme_acct.c')
-rw-r--r-- | modules/md/md_acme_acct.c | 785 |
1 files changed, 432 insertions, 353 deletions
diff --git a/modules/md/md_acme_acct.c b/modules/md/md_acme_acct.c index c4a2b5f..f3e043e 100644 --- a/modules/md/md_acme_acct.c +++ b/modules/md/md_acme_acct.c @@ -30,6 +30,7 @@ #include "md_json.h" #include "md_jws.h" #include "md_log.h" +#include "md_result.h" #include "md_store.h" #include "md_util.h" #include "md_version.h" @@ -38,15 +39,12 @@ #include "md_acme_acct.h" static apr_status_t acct_make(md_acme_acct_t **pacct, apr_pool_t *p, - const char *ca_url, const char *id, apr_array_header_t *contacts) + const char *ca_url, apr_array_header_t *contacts) { md_acme_acct_t *acct; acct = apr_pcalloc(p, sizeof(*acct)); - - acct->id = id? apr_pstrdup(p, id) : NULL; acct->ca_url = ca_url; - if (!contacts || apr_is_empty_array(contacts)) { acct->contacts = apr_array_make(p, 5, sizeof(const char *)); } @@ -72,87 +70,118 @@ static const char *mk_acct_pattern(apr_pool_t *p, md_acme_t *acme) /**************************************************************************************************/ /* json load/save */ -static md_json_t *acct_to_json(md_acme_acct_t *acct, apr_pool_t *p) +static md_acme_acct_st acct_st_from_str(const char *s) +{ + if (s) { + if (!strcmp("valid", s)) { + return MD_ACME_ACCT_ST_VALID; + } + else if (!strcmp("deactivated", s)) { + return MD_ACME_ACCT_ST_DEACTIVATED; + } + else if (!strcmp("revoked", s)) { + return MD_ACME_ACCT_ST_REVOKED; + } + } + return MD_ACME_ACCT_ST_UNKNOWN; +} + +md_json_t *md_acme_acct_to_json(md_acme_acct_t *acct, apr_pool_t *p) { md_json_t *jacct; + const char *s; assert(acct); jacct = md_json_create(p); - md_json_sets(acct->id, jacct, MD_KEY_ID, NULL); - md_json_setb(acct->disabled, jacct, MD_KEY_DISABLED, NULL); - md_json_sets(acct->url, jacct, MD_KEY_URL, NULL); - md_json_sets(acct->ca_url, jacct, MD_KEY_CA_URL, NULL); - md_json_setj(acct->registration, jacct, MD_KEY_REGISTRATION, NULL); - if (acct->agreement) { - md_json_sets(acct->agreement, jacct, MD_KEY_AGREEMENT, NULL); - } - + switch (acct->status) { + case MD_ACME_ACCT_ST_VALID: + s = "valid"; + break; + case MD_ACME_ACCT_ST_DEACTIVATED: + s = "deactivated"; + break; + case MD_ACME_ACCT_ST_REVOKED: + s = "revoked"; + break; + default: + s = NULL; + break; + } + if (s) md_json_sets(s, jacct, MD_KEY_STATUS, NULL); + if (acct->url) md_json_sets(acct->url, jacct, MD_KEY_URL, NULL); + if (acct->ca_url) md_json_sets(acct->ca_url, jacct, MD_KEY_CA_URL, NULL); + if (acct->contacts) md_json_setsa(acct->contacts, jacct, MD_KEY_CONTACT, NULL); + if (acct->registration) md_json_setj(acct->registration, jacct, MD_KEY_REGISTRATION, NULL); + if (acct->agreement) md_json_sets(acct->agreement, jacct, MD_KEY_AGREEMENT, NULL); + if (acct->orders) md_json_sets(acct->orders, jacct, MD_KEY_ORDERS, NULL); + if (acct->eab_kid) md_json_sets(acct->eab_kid, jacct, MD_KEY_EAB, MD_KEY_KID, NULL); + if (acct->eab_hmac) md_json_sets(acct->eab_hmac, jacct, MD_KEY_EAB, MD_KEY_HMAC, NULL); + return jacct; } -static apr_status_t acct_from_json(md_acme_acct_t **pacct, md_json_t *json, apr_pool_t *p) +apr_status_t md_acme_acct_from_json(md_acme_acct_t **pacct, md_json_t *json, apr_pool_t *p) { apr_status_t rv = APR_EINVAL; md_acme_acct_t *acct; - int disabled; - const char *ca_url, *url, *id; + md_acme_acct_st status = MD_ACME_ACCT_ST_UNKNOWN; + const char *ca_url, *url; apr_array_header_t *contacts; - id = md_json_gets(json, MD_KEY_ID, NULL); - disabled = md_json_getb(json, MD_KEY_DISABLED, NULL); - ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL); - if (!ca_url) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no CA url: %s", id); - goto out; + if (md_json_has_key(json, MD_KEY_STATUS, NULL)) { + status = acct_st_from_str(md_json_gets(json, MD_KEY_STATUS, NULL)); } - + url = md_json_gets(json, MD_KEY_URL, NULL); if (!url) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no url: %s", id); - goto out; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no url"); + goto leave; } + ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL); + if (!ca_url) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no CA url: %s", url); + goto leave; + } + contacts = apr_array_make(p, 5, sizeof(const char *)); - md_json_getsa(contacts, json, MD_KEY_REGISTRATION, MD_KEY_CONTACT, NULL); - rv = acct_make(&acct, p, ca_url, id, contacts); - if (APR_SUCCESS == rv) { - acct->disabled = disabled; - acct->url = url; + if (md_json_has_key(json, MD_KEY_CONTACT, NULL)) { + md_json_getsa(contacts, json, MD_KEY_CONTACT, NULL); + } + else { + md_json_getsa(contacts, json, MD_KEY_REGISTRATION, MD_KEY_CONTACT, NULL); + } + rv = acct_make(&acct, p, ca_url, contacts); + if (APR_SUCCESS != rv) goto leave; + + acct->status = status; + acct->url = url; + acct->agreement = md_json_gets(json, MD_KEY_AGREEMENT, NULL); + if (!acct->agreement) { + /* backward compatible check */ acct->agreement = md_json_gets(json, "terms-of-service", NULL); } + acct->orders = md_json_gets(json, MD_KEY_ORDERS, NULL); + if (md_json_has_key(json, MD_KEY_EAB, MD_KEY_KID, NULL) + && md_json_has_key(json, MD_KEY_EAB, MD_KEY_HMAC, NULL)) { + acct->eab_kid = md_json_gets(json, MD_KEY_EAB, MD_KEY_KID, NULL); + acct->eab_hmac = md_json_gets(json, MD_KEY_EAB, MD_KEY_HMAC, NULL); + } -out: +leave: *pacct = (APR_SUCCESS == rv)? acct : NULL; return rv; } -apr_status_t md_acme_acct_save_staged(md_acme_t *acme, md_store_t *store, md_t *md, apr_pool_t *p) -{ - md_acme_acct_t *acct = acme->acct; - md_json_t *jacct; - apr_status_t rv; - - jacct = acct_to_json(acct, p); - - rv = md_store_save(store, p, MD_SG_STAGING, md->name, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 0); - if (APR_SUCCESS == rv) { - rv = md_store_save(store, p, MD_SG_STAGING, md->name, MD_FN_ACCT_KEY, - MD_SV_PKEY, acme->acct_key, 0); - } - return rv; -} - apr_status_t md_acme_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme, - md_acme_acct_t *acct, md_pkey_t *acct_key) + const char **pid, md_acme_acct_t *acct, md_pkey_t *acct_key) { md_json_t *jacct; apr_status_t rv; int i; - const char *id; - - jacct = acct_to_json(acct, p); - id = acct->id; + const char *id = pid? *pid : NULL; + jacct = md_acme_acct_to_json(acct, p); if (id) { rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 0); } @@ -160,23 +189,16 @@ apr_status_t md_acme_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme rv = APR_EAGAIN; for (i = 0; i < 1000 && APR_SUCCESS != rv; ++i) { id = mk_acct_id(p, acme, i); - md_json_sets(id, jacct, MD_KEY_ID, NULL); rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 1); } - } if (APR_SUCCESS == rv) { - acct->id = id; + if (pid) *pid = id; rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCT_KEY, MD_SV_PKEY, acct_key, 0); } return rv; } -apr_status_t md_acme_save(md_acme_t *acme, md_store_t *store, apr_pool_t *p) -{ - return md_acme_acct_save(store, p, acme, acme->acct, acme->acct_key); -} - apr_status_t md_acme_acct_load(md_acme_acct_t **pacct, md_pkey_t **ppkey, md_store_t *store, md_store_group_t group, const char *name, apr_pool_t *p) @@ -193,11 +215,11 @@ apr_status_t md_acme_acct_load(md_acme_acct_t **pacct, md_pkey_t **ppkey, goto out; } - rv = acct_from_json(pacct, json, p); + rv = md_acme_acct_from_json(pacct, json, p); if (APR_SUCCESS == rv) { rv = md_store_load(store, group, name, MD_FN_ACCT_KEY, MD_SV_PKEY, (void**)ppkey, p); if (APR_SUCCESS != rv) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "loading key: %s", name); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "loading key: %s", name); goto out; } } @@ -212,9 +234,36 @@ out: /**************************************************************************************************/ /* Lookup */ +int md_acme_acct_matches_url(md_acme_acct_t *acct, const char *url) +{ + /* The ACME url must match exactly */ + if (!url || !acct->ca_url || strcmp(acct->ca_url, url)) return 0; + return 1; +} + +int md_acme_acct_matches_md(md_acme_acct_t *acct, const md_t *md) +{ + if (!md_acme_acct_matches_url(acct, md->ca_effective)) return 0; + /* if eab values are not mentioned, we match an account regardless + * if it was registered with eab or not */ + if (!md->ca_eab_kid || !md->ca_eab_hmac) { + /* No eab only acceptable when no eab is asked for. + * This prevents someone that has no external account binding + * to re-use an account from another MDomain that was created + * with a binding. */ + return !acct->eab_kid || !acct->eab_hmac; + } + /* But of eab is asked for, we need an acct that matches exactly. + * When someone configures a new EAB and we need + * to created a new account for it. */ + if (!acct->eab_kid || !acct->eab_hmac) return 0; + return !strcmp(acct->eab_kid, md->ca_eab_kid) + && !strcmp(acct->eab_hmac, md->ca_eab_hmac); +} + typedef struct { apr_pool_t *p; - md_acme_t *acme; + const md_t *md; const char *id; } find_ctx; @@ -222,232 +271,227 @@ static int find_acct(void *baton, const char *name, const char *aspect, md_store_vtype_t vtype, void *value, apr_pool_t *ptemp) { find_ctx *ctx = baton; - int disabled; - const char *ca_url, *id; - + md_acme_acct_t *acct; + apr_status_t rv; + (void)aspect; (void)ptemp; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, "account candidate %s/%s", name, aspect); if (MD_SV_JSON == vtype) { - md_json_t *json = value; - - id = md_json_gets(json, MD_KEY_ID, NULL); - disabled = md_json_getb(json, MD_KEY_DISABLED, NULL); - ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL); - - if (!disabled && ca_url && !strcmp(ctx->acme->url, ca_url)) { + rv = md_acme_acct_from_json(&acct, (md_json_t*)value, ptemp); + if (APR_SUCCESS != rv) goto cleanup; + + if (MD_ACME_ACCT_ST_VALID == acct->status + && (!ctx->md || md_acme_acct_matches_md(acct, ctx->md))) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, - "found account %s for %s: %s, disabled=%d, ca-url=%s", - name, ctx->acme->url, id, disabled, ca_url); - ctx->id = id; + "found account %s for %s: %s, status=%d", + acct->id, ctx->md->ca_effective, aspect, acct->status); + ctx->id = apr_pstrdup(ctx->p, name); return 0; } } +cleanup: return 1; } -static apr_status_t acct_find(md_acme_acct_t **pacct, md_pkey_t **ppkey, - md_store_t *store, md_acme_t *acme, apr_pool_t *p) +static apr_status_t acct_find(const char **pid, md_acme_acct_t **pacct, md_pkey_t **ppkey, + md_store_t *store, md_store_group_t group, + const char *name_pattern, + const md_t *md, apr_pool_t *p) { apr_status_t rv; find_ctx ctx; - + + memset(&ctx, 0, sizeof(ctx)); ctx.p = p; - ctx.acme = acme; - ctx.id = NULL; - - rv = md_store_iter(find_acct, &ctx, store, p, MD_SG_ACCOUNTS, mk_acct_pattern(p, acme), - MD_FN_ACCOUNT, MD_SV_JSON); + ctx.md = md; + + rv = md_store_iter(find_acct, &ctx, store, p, group, name_pattern, MD_FN_ACCOUNT, MD_SV_JSON); if (ctx.id) { - rv = md_acme_acct_load(pacct, ppkey, store, MD_SG_ACCOUNTS, ctx.id, p); + *pid = ctx.id; + rv = md_acme_acct_load(pacct, ppkey, store, group, ctx.id, p); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "acct_find: got account %s", ctx.id); } else { *pacct = NULL; rv = APR_ENOENT; + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "acct_find: none found"); } - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, - "acct_find %s", (*pacct)? (*pacct)->id : "NULL"); return rv; } -/**************************************************************************************************/ -/* Register a new account */ - -typedef struct { - md_acme_t *acme; - apr_pool_t *p; -} acct_ctx_t; - -static apr_status_t on_init_acct_new(md_acme_req_t *req, void *baton) +static apr_status_t acct_find_and_verify(md_store_t *store, md_store_group_t group, + const char *name_pattern, + md_acme_t *acme, const md_t *md, + apr_pool_t *p) { - acct_ctx_t *ctx = baton; - md_json_t *jpayload; + md_acme_acct_t *acct; + md_pkey_t *pkey; + const char *id; + apr_status_t rv; - jpayload = md_json_create(req->p); - md_json_sets("new-reg", jpayload, MD_KEY_RESOURCE, NULL); - md_json_setsa(ctx->acme->acct->contacts, jpayload, MD_KEY_CONTACT, NULL); - if (ctx->acme->acct->agreement) { - md_json_sets(ctx->acme->acct->agreement, jpayload, MD_KEY_AGREEMENT, NULL); - } - - return md_acme_req_body_init(req, jpayload); -} + rv = acct_find(&id, &acct, &pkey, store, group, name_pattern, md, p); + if (APR_SUCCESS == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, p, "acct_find_and_verify: found %s", + id); + acme->acct_id = (MD_SG_STAGING == group)? NULL : id; + acme->acct = acct; + acme->acct_key = pkey; + rv = md_acme_acct_validate(acme, (MD_SG_STAGING == group)? NULL : store, p); + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, p, "acct_find_and_verify: verified %s", + id); -static apr_status_t acct_upd(md_acme_t *acme, apr_pool_t *p, - const apr_table_t *hdrs, md_json_t *body, void *baton) -{ - acct_ctx_t *ctx = baton; - apr_status_t rv = APR_SUCCESS; - md_acme_acct_t *acct = acme->acct; - - if (!acct->url) { - const char *location = apr_table_get(hdrs, "location"); - if (!location) { - md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, APR_EINVAL, p, "new acct without location"); - return APR_EINVAL; - } - acct->url = apr_pstrdup(ctx->p, location); - } - if (!acct->tos_required) { - acct->tos_required = md_link_find_relation(hdrs, ctx->p, "terms-of-service"); - if (acct->tos_required) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, - "server requires agreement to <%s>", acct->tos_required); + if (APR_SUCCESS != rv) { + acme->acct_id = NULL; + acme->acct = NULL; + acme->acct_key = NULL; + if (APR_STATUS_IS_ENOENT(rv)) { + /* verification failed and account has been disabled. + Indicate to caller that he may try again. */ + rv = APR_EAGAIN; + } } } - - apr_array_clear(acct->contacts); - md_json_getsa(acct->contacts, body, MD_KEY_CONTACT, NULL); - acct->registration = md_json_clone(ctx->p, body); - - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "updated acct %s", acct->url); return rv; } -static apr_status_t acct_register(md_acme_t *acme, apr_pool_t *p, - apr_array_header_t *contacts, const char *agreement) +apr_status_t md_acme_find_acct_for_md(md_acme_t *acme, md_store_t *store, const md_t *md) { apr_status_t rv; - md_pkey_t *pkey; - const char *err = NULL, *uri; - md_pkey_spec_t spec; - int i; - - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "create new account"); - if (agreement) { - if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, agreement, &err))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, - "invalid agreement uri (%s): %s", err, agreement); - goto out; - } + while (APR_EAGAIN == (rv = acct_find_and_verify(store, MD_SG_ACCOUNTS, + mk_acct_pattern(acme->p, acme), + acme, md, acme->p))) { + /* nop */ } - for (i = 0; i < contacts->nelts; ++i) { - uri = APR_ARRAY_IDX(contacts, i, const char *); - if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, uri, &err))) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, - "invalid contact uri (%s): %s", err, uri); - goto out; + + if (APR_STATUS_IS_ENOENT(rv)) { + /* No suitable account found in MD_SG_ACCOUNTS. Maybe a new account + * can already be found in MD_SG_STAGING? */ + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, + "no account found, looking in STAGING"); + rv = acct_find_and_verify(store, MD_SG_STAGING, "*", acme, md, acme->p); + if (APR_EAGAIN == rv) { + rv = APR_ENOENT; } } - - spec.type = MD_PKEY_TYPE_RSA; - spec.params.rsa.bits = MD_ACME_ACCT_PKEY_BITS; - - if (APR_SUCCESS == (rv = md_pkey_gen(&pkey, acme->p, &spec)) - && APR_SUCCESS == (rv = acct_make(&acme->acct, p, acme->url, NULL, contacts))) { - acct_ctx_t ctx; + return rv; +} - acme->acct_key = pkey; - if (agreement) { - acme->acct->agreement = agreement; - } +apr_status_t md_acme_acct_id_for_md(const char **pid, md_store_t *store, + md_store_group_t group, const md_t *md, + apr_pool_t *p) +{ + apr_status_t rv; + find_ctx ctx; - ctx.acme = acme; - ctx.p = p; - rv = md_acme_POST(acme, acme->new_reg, on_init_acct_new, acct_upd, NULL, &ctx); - if (APR_SUCCESS == rv) { - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, - "registered new account %s", acme->acct->url); - } - } + memset(&ctx, 0, sizeof(ctx)); + ctx.p = p; + ctx.md = md; -out: - if (APR_SUCCESS != rv && acme->acct) { - acme->acct = NULL; + rv = md_store_iter(find_acct, &ctx, store, p, group, "*", MD_FN_ACCOUNT, MD_SV_JSON); + if (ctx.id) { + *pid = ctx.id; + rv = APR_SUCCESS; } + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "acct_id_for_md %s -> %s", md->name, *pid); return rv; } /**************************************************************************************************/ -/* acct validation */ +/* acct operation context */ +typedef struct { + md_acme_t *acme; + apr_pool_t *p; + const char *agreement; + const char *eab_kid; + const char *eab_hmac; +} acct_ctx_t; -static apr_status_t on_init_acct_valid(md_acme_req_t *req, void *baton) -{ - md_json_t *jpayload; +/**************************************************************************************************/ +/* acct update */ +static apr_status_t on_init_acct_upd(md_acme_req_t *req, void *baton) +{ (void)baton; - jpayload = md_json_create(req->p); - md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL); - - return md_acme_req_body_init(req, jpayload); + return md_acme_req_body_init(req, NULL); } -static apr_status_t acct_valid(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, - md_json_t *body, void *baton) +static apr_status_t acct_upd(md_acme_t *acme, apr_pool_t *p, + const apr_table_t *hdrs, md_json_t *body, void *baton) { - md_acme_acct_t *acct = acme->acct; + acct_ctx_t *ctx = baton; apr_status_t rv = APR_SUCCESS; - const char *body_str; - const char *tos_required; + md_acme_acct_t *acct = acme->acct; + + if (md_log_is_level(p, MD_LOG_TRACE2)) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, acme->p, "acct update response: %s", + md_json_writep(body, p, MD_JSON_FMT_COMPACT)); + } + + if (!acct->url) { + const char *location = apr_table_get(hdrs, "location"); + if (!location) { + md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, APR_EINVAL, p, "new acct without location"); + return APR_EINVAL; + } + acct->url = apr_pstrdup(ctx->p, location); + } - (void)p; - (void)baton; apr_array_clear(acct->contacts); - md_json_getsa(acct->contacts, body, MD_KEY_CONTACT, NULL); - acct->registration = md_json_clone(acme->p, body); - - body_str = md_json_writep(body, acme->p, MD_JSON_FMT_INDENT); - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, "validate acct %s: %s", - acct->url, body_str ? body_str : "<failed to serialize!>"); - - acct->agreement = md_json_gets(acct->registration, MD_KEY_AGREEMENT, NULL); - tos_required = md_link_find_relation(hdrs, acme->p, "terms-of-service"); - - if (tos_required) { - if (!acct->agreement || strcmp(tos_required, acct->agreement)) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, - "needs to agree to terms-of-service '%s', " - "has already agreed to '%s'", - tos_required, acct->agreement); - } - acct->tos_required = tos_required; + md_json_dupsa(acct->contacts, acme->p, body, MD_KEY_CONTACT, NULL); + if (md_json_has_key(body, MD_KEY_STATUS, NULL)) { + acct->status = acct_st_from_str(md_json_gets(body, MD_KEY_STATUS, NULL)); + } + if (md_json_has_key(body, MD_KEY_AGREEMENT, NULL)) { + acct->agreement = md_json_dups(acme->p, body, MD_KEY_AGREEMENT, NULL); } + if (md_json_has_key(body, MD_KEY_ORDERS, NULL)) { + acct->orders = md_json_dups(acme->p, body, MD_KEY_ORDERS, NULL); + } + if (ctx->eab_kid && ctx->eab_hmac) { + acct->eab_kid = ctx->eab_kid; + acct->eab_hmac = ctx->eab_hmac; + } + acct->registration = md_json_clone(ctx->p, body); + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "updated acct %s", acct->url); return rv; } -static apr_status_t md_acme_validate_acct(md_acme_t *acme) +apr_status_t md_acme_acct_update(md_acme_t *acme) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "acct validation"); + acct_ctx_t ctx; + + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "acct update"); if (!acme->acct) { return APR_EINVAL; } - return md_acme_POST(acme, acme->acct->url, on_init_acct_valid, acct_valid, NULL, NULL); + memset(&ctx, 0, sizeof(ctx)); + ctx.acme = acme; + ctx.p = acme->p; + return md_acme_POST(acme, acme->acct->url, on_init_acct_upd, acct_upd, NULL, NULL, &ctx); } -/**************************************************************************************************/ -/* account setup */ - -static apr_status_t acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t *p) +apr_status_t md_acme_acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t *p) { apr_status_t rv; - if (APR_SUCCESS != (rv = md_acme_validate_acct(acme))) { - if (acme->acct && (APR_ENOENT == rv || APR_EACCES == rv)) { - if (!acme->acct->disabled) { - acme->acct->disabled = 1; + if (APR_SUCCESS != (rv = md_acme_acct_update(acme))) { + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, acme->p, + "acct update failed for %s", acme->acct->url); + if (APR_EINVAL == rv && (acme->acct->agreement || !acme->ca_agreement)) { + /* Sadly, some proprietary ACME servers choke on empty POSTs + * on accounts. Try a faked ToS agreement. */ + md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, acme->p, + "trying acct update via ToS agreement"); + rv = md_acme_agree(acme, p, "accepted"); + } + if (acme->acct && (APR_ENOENT == rv || APR_EACCES == rv || APR_EINVAL == rv)) { + if (MD_ACME_ACCT_ST_VALID == acme->acct->status) { + acme->acct->status = MD_ACME_ACCT_ST_UNKNOWN; if (store) { - md_acme_save(acme, store, p); + md_acme_acct_save(store, p, acme, &acme->acct_id, acme->acct, acme->acct_key); } } acme->acct = NULL; @@ -458,133 +502,187 @@ static apr_status_t acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t return rv; } -apr_status_t md_acme_use_acct(md_acme_t *acme, md_store_t *store, - apr_pool_t *p, const char *acct_id) +/**************************************************************************************************/ +/* Register a new account */ + +static apr_status_t get_eab(md_json_t **peab, md_acme_req_t *req, const char *kid, + const char *hmac64, md_pkey_t *account_key, + const char *url) { - md_acme_acct_t *acct; - md_pkey_t *pkey; + md_json_t *eab, *prot_fields, *jwk; + md_data_t payload, hmac_key; apr_status_t rv; - - if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey, - store, MD_SG_ACCOUNTS, acct_id, acme->p))) { - if (acct->ca_url && !strcmp(acct->ca_url, acme->url)) { - acme->acct = acct; - acme->acct_key = pkey; - rv = acct_validate(acme, store, p); - } - else { - /* account is from a nother server or, more likely, from another - * protocol endpoint on the same server */ - rv = APR_ENOENT; - } + + prot_fields = md_json_create(req->p); + md_json_sets(url, prot_fields, "url", NULL); + md_json_sets(kid, prot_fields, "kid", NULL); + + rv = md_jws_get_jwk(&jwk, req->p, account_key); + if (APR_SUCCESS != rv) goto cleanup; + + md_data_null(&payload); + payload.data = md_json_writep(jwk, req->p, MD_JSON_FMT_COMPACT); + if (!payload.data) { + rv = APR_EINVAL; + goto cleanup; } - return rv; -} + payload.len = strlen(payload.data); -apr_status_t md_acme_use_acct_staged(md_acme_t *acme, struct md_store_t *store, - md_t *md, apr_pool_t *p) -{ - 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_STAGING, md->name, acme->p))) { - acme->acct = acct; - acme->acct_key = pkey; - rv = acct_validate(acme, NULL, p); + md_util_base64url_decode(&hmac_key, hmac64, req->p); + if (!hmac_key.len) { + rv = APR_EINVAL; + md_result_problem_set(req->result, rv, "apache:eab-hmac-invalid", + "external account binding HMAC value is not valid base64", NULL); + goto cleanup; } + + rv = md_jws_hmac(&eab, req->p, &payload, prot_fields, &hmac_key); + if (APR_SUCCESS != rv) { + md_result_problem_set(req->result, rv, "apache:eab-hmac-fail", + "external account binding MAC could not be computed", NULL); + } + +cleanup: + *peab = (APR_SUCCESS == rv)? eab : NULL; return rv; } -const char *md_acme_get_acct_id(md_acme_t *acme) +static apr_status_t on_init_acct_new(md_acme_req_t *req, void *baton) { - return acme->acct? acme->acct->id : NULL; -} + acct_ctx_t *ctx = baton; + md_json_t *jpayload, *jeab; + apr_status_t rv; -const char *md_acme_get_agreement(md_acme_t *acme) -{ - return acme->acct? acme->acct->agreement : NULL; -} + jpayload = md_json_create(req->p); + md_json_setsa(ctx->acme->acct->contacts, jpayload, MD_KEY_CONTACT, NULL); + if (ctx->agreement) { + md_json_setb(1, jpayload, "termsOfServiceAgreed", NULL); + } + if (ctx->eab_kid && ctx->eab_hmac) { + rv = get_eab(&jeab, req, ctx->eab_kid, ctx->eab_hmac, + req->acme->acct_key, req->url); + if (APR_SUCCESS != rv) goto cleanup; + md_json_setj(jeab, jpayload, "externalAccountBinding", NULL); + } + rv = md_acme_req_body_init(req, jpayload); + +cleanup: + return rv; +} -apr_status_t md_acme_find_acct(md_acme_t *acme, md_store_t *store, apr_pool_t *p) +apr_status_t md_acme_acct_register(md_acme_t *acme, md_store_t *store, + const md_t *md, apr_pool_t *p) { - md_acme_acct_t *acct; - md_pkey_t *pkey; apr_status_t rv; + md_pkey_t *pkey; + const char *err = NULL, *uri; + md_pkey_spec_t spec; + int i; + acct_ctx_t ctx; - while (APR_SUCCESS == acct_find(&acct, &pkey, store, acme, acme->p)) { - acme->acct = acct; - acme->acct_key = pkey; - rv = acct_validate(acme, store, p); - - if (APR_SUCCESS == rv) { - return rv; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "create new account"); + + memset(&ctx, 0, sizeof(ctx)); + ctx.acme = acme; + ctx.p = p; + /* The agreement URL is submitted when the ACME server announces Terms-of-Service + * in its directory meta data. The magic value "accepted" will always use the + * advertised URL. */ + ctx.agreement = NULL; + if (acme->ca_agreement && md->ca_agreement) { + ctx.agreement = !strcmp("accepted", md->ca_agreement)? + acme->ca_agreement : md->ca_agreement; + } + + if (ctx.agreement) { + if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, ctx.agreement, &err))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "invalid agreement uri (%s): %s", err, ctx.agreement); + goto out; } - else { - acme->acct = NULL; - acme->acct_key = NULL; - if (!APR_STATUS_IS_ENOENT(rv)) { - /* encountered error with server */ - return rv; + } + ctx.eab_kid = md->ca_eab_kid; + ctx.eab_hmac = md->ca_eab_hmac; + + for (i = 0; i < md->contacts->nelts; ++i) { + uri = APR_ARRAY_IDX(md->contacts, i, const char *); + if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, uri, &err))) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, + "invalid contact uri (%s): %s", err, uri); + goto out; + } + } + + /* If there is no key selected yet, try to find an existing one for the same host. + * Let's Encrypt identifies accounts by their key for their ACMEv1 and v2 services. + * Although the account appears on both services with different urls, it is + * internally the same one. + * I think this is beneficial if someone migrates from ACMEv1 to v2 and not a leak + * of identifying information. + */ + if (!acme->acct_key) { + find_ctx fctx; + + memset(&fctx, 0, sizeof(fctx)); + fctx.p = p; + fctx.md = md; + + md_store_iter(find_acct, &fctx, store, p, MD_SG_ACCOUNTS, + mk_acct_pattern(p, acme), MD_FN_ACCOUNT, MD_SV_JSON); + if (fctx.id) { + rv = md_store_load(store, MD_SG_ACCOUNTS, fctx.id, MD_FN_ACCT_KEY, MD_SV_PKEY, + (void**)&acme->acct_key, p); + if (APR_SUCCESS == rv) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, + "reusing key from account %s", fctx.id); + } + else { + acme->acct_key = NULL; } } } - return APR_ENOENT; -} - -apr_status_t md_acme_create_acct(md_acme_t *acme, apr_pool_t *p, apr_array_header_t *contacts, - const char *agreement) -{ - return acct_register(acme, p, contacts, agreement); -} - -/**************************************************************************************************/ -/* Delete the account */ - -apr_status_t md_acme_unstore_acct(md_store_t *store, apr_pool_t *p, const char *acct_id) -{ - apr_status_t rv = APR_SUCCESS; - rv = md_store_remove(store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCOUNT, p, 1); + /* If we still have no key, generate a new one */ + if (!acme->acct_key) { + spec.type = MD_PKEY_TYPE_RSA; + spec.params.rsa.bits = MD_ACME_ACCT_PKEY_BITS; + + if (APR_SUCCESS != (rv = md_pkey_gen(&pkey, acme->p, &spec))) goto out; + acme->acct_key = pkey; + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "created new account key"); + } + + if (APR_SUCCESS != (rv = acct_make(&acme->acct, p, acme->url, md->contacts))) goto out; + rv = md_acme_POST_new_account(acme, on_init_acct_new, acct_upd, NULL, NULL, &ctx); if (APR_SUCCESS == rv) { - md_store_remove(store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCT_KEY, p, 1); + md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, + "registered new account %s", acme->acct->url); + } + +out: + if (APR_SUCCESS != rv && acme->acct) { + acme->acct = NULL; } return rv; } +/**************************************************************************************************/ +/* Deactivate the account */ + static apr_status_t on_init_acct_del(md_acme_req_t *req, void *baton) { md_json_t *jpayload; (void)baton; jpayload = md_json_create(req->p); - md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL); - md_json_setb(1, jpayload, "delete", NULL); - + md_json_sets("deactivated", jpayload, MD_KEY_STATUS, NULL); return md_acme_req_body_init(req, jpayload); } -static apr_status_t acct_del(md_acme_t *acme, apr_pool_t *p, - const apr_table_t *hdrs, md_json_t *body, void *baton) -{ - md_store_t *store = baton; - apr_status_t rv = APR_SUCCESS; - - (void)hdrs; - (void)body; - md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, "deleted account %s", acme->acct->url); - if (store) { - rv = md_acme_unstore_acct(store, p, acme->acct->id); - acme->acct = NULL; - acme->acct_key = NULL; - } - return rv; -} - -apr_status_t md_acme_delete_acct(md_acme_t *acme, md_store_t *store, apr_pool_t *p) +apr_status_t md_acme_acct_deactivate(md_acme_t *acme, apr_pool_t *p) { md_acme_acct_t *acct = acme->acct; + acct_ctx_t ctx; (void)p; if (!acct) { @@ -592,7 +690,10 @@ apr_status_t md_acme_delete_acct(md_acme_t *acme, md_store_t *store, apr_pool_t } md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "delete account %s from %s", acct->url, acct->ca_url); - return md_acme_POST(acme, acct->url, on_init_acct_del, acct_del, NULL, store); + memset(&ctx, 0, sizeof(ctx)); + ctx.acme = acme; + ctx.p = p; + return md_acme_POST(acme, acct->url, on_init_acct_del, acct_upd, NULL, NULL, &ctx); } /**************************************************************************************************/ @@ -604,9 +705,9 @@ static apr_status_t on_init_agree_tos(md_acme_req_t *req, void *baton) md_json_t *jpayload; jpayload = md_json_create(req->p); - md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL); - md_json_sets(ctx->acme->acct->agreement, jpayload, MD_KEY_AGREEMENT, NULL); - + if (ctx->acme->acct->agreement) { + md_json_setb(1, jpayload, "termsOfServiceAgreed", NULL); + } return md_acme_req_body_init(req, jpayload); } @@ -615,21 +716,14 @@ apr_status_t md_acme_agree(md_acme_t *acme, apr_pool_t *p, const char *agreement acct_ctx_t ctx; acme->acct->agreement = agreement; + if (!strcmp("accepted", agreement) && acme->ca_agreement) { + acme->acct->agreement = acme->ca_agreement; + } + + memset(&ctx, 0, sizeof(ctx)); ctx.acme = acme; ctx.p = p; - return md_acme_POST(acme, acme->acct->url, on_init_agree_tos, acct_upd, NULL, &ctx); -} - -static int agreement_required(md_acme_acct_t *acct) -{ - /* We used to really check if the account agreement and the one - * indicated as valid are the very same: - * return (!acct->agreement - * || (acct->tos_required && strcmp(acct->tos_required, acct->agreement))); - * However, LE is happy if the account has agreed to a ToS in the past and - * does not required a renewed acceptance. - */ - return !acct->agreement; + return md_acme_POST(acme, acme->acct->url, on_init_agree_tos, acct_upd, NULL, NULL, &ctx); } apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p, @@ -637,32 +731,17 @@ apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p, { apr_status_t rv = APR_SUCCESS; - /* Check if (correct) Terms-of-Service for account were accepted */ + /* We used to really check if the account agreement and the one indicated in meta + * are the very same. However, LE is happy if the account has agreed to a ToS in + * the past and does not require a renewed acceptance. + */ *prequired = NULL; - if (agreement_required(acme->acct)) { - const char *tos = acme->acct->tos_required; - if (!tos) { - if (APR_SUCCESS != (rv = md_acme_validate_acct(acme))) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, - "validate for account %s", acme->acct->id); - return rv; - } - tos = acme->acct->tos_required; - if (!tos) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, "unknown terms-of-service " - "required after validation of account %s", acme->acct->id); - return APR_EGENERAL; - } - } - - if (acme->acct->agreement && !strcmp(tos, acme->acct->agreement)) { - rv = md_acme_agree(acme, p, tos); - } - else if (agreement && !strcmp(tos, agreement)) { - rv = md_acme_agree(acme, p, tos); + if (!acme->acct->agreement && acme->ca_agreement) { + if (agreement) { + rv = md_acme_agree(acme, p, acme->ca_agreement); } else { - *prequired = apr_pstrdup(p, tos); + *prequired = acme->ca_agreement; rv = APR_INCOMPLETE; } } |