750 lines
25 KiB
C
750 lines
25 KiB
C
/* 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 <stdio.h>
|
|
|
|
#include <apr_lib.h>
|
|
#include <apr_file_info.h>
|
|
#include <apr_file_io.h>
|
|
#include <apr_fnmatch.h>
|
|
#include <apr_hash.h>
|
|
#include <apr_strings.h>
|
|
#include <apr_tables.h>
|
|
|
|
#include "md.h"
|
|
#include "md_crypt.h"
|
|
#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"
|
|
|
|
#include "md_acme.h"
|
|
#include "md_acme_acct.h"
|
|
|
|
static apr_status_t acct_make(md_acme_acct_t **pacct, apr_pool_t *p,
|
|
const char *ca_url, apr_array_header_t *contacts)
|
|
{
|
|
md_acme_acct_t *acct;
|
|
|
|
acct = apr_pcalloc(p, sizeof(*acct));
|
|
acct->ca_url = ca_url;
|
|
if (!contacts || apr_is_empty_array(contacts)) {
|
|
acct->contacts = apr_array_make(p, 5, sizeof(const char *));
|
|
}
|
|
else {
|
|
acct->contacts = apr_array_copy(p, contacts);
|
|
}
|
|
|
|
*pacct = acct;
|
|
return APR_SUCCESS;
|
|
}
|
|
|
|
|
|
static const char *mk_acct_id(apr_pool_t *p, md_acme_t *acme, int i)
|
|
{
|
|
return apr_psprintf(p, "ACME-%s-%04d", acme->sname, i);
|
|
}
|
|
|
|
static const char *mk_acct_pattern(apr_pool_t *p, md_acme_t *acme)
|
|
{
|
|
return apr_psprintf(p, "ACME-%s-*", acme->sname);
|
|
}
|
|
|
|
/**************************************************************************************************/
|
|
/* json load/save */
|
|
|
|
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);
|
|
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;
|
|
}
|
|
|
|
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;
|
|
md_acme_acct_st status = MD_ACME_ACCT_ST_UNKNOWN;
|
|
const char *ca_url, *url;
|
|
apr_array_header_t *contacts;
|
|
|
|
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");
|
|
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 *));
|
|
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);
|
|
}
|
|
|
|
leave:
|
|
*pacct = (APR_SUCCESS == rv)? acct : NULL;
|
|
return rv;
|
|
}
|
|
|
|
apr_status_t md_acme_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme,
|
|
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 = 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);
|
|
}
|
|
else {
|
|
rv = APR_EAGAIN;
|
|
for (i = 0; i < 1000 && APR_SUCCESS != rv; ++i) {
|
|
id = mk_acct_id(p, acme, i);
|
|
rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 1);
|
|
}
|
|
}
|
|
if (APR_SUCCESS == rv) {
|
|
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_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)
|
|
{
|
|
md_json_t *json;
|
|
apr_status_t rv;
|
|
|
|
rv = md_store_load_json(store, group, name, MD_FN_ACCOUNT, &json, p);
|
|
if (APR_STATUS_IS_ENOENT(rv)) {
|
|
goto out;
|
|
}
|
|
if (APR_SUCCESS != rv) {
|
|
md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "error reading account: %s", name);
|
|
goto out;
|
|
}
|
|
|
|
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, rv, p, "loading key: %s", name);
|
|
goto out;
|
|
}
|
|
}
|
|
out:
|
|
if (APR_SUCCESS != rv) {
|
|
*pacct = NULL;
|
|
*ppkey = NULL;
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
/**************************************************************************************************/
|
|
/* 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;
|
|
const md_t *md;
|
|
const char *id;
|
|
} find_ctx;
|
|
|
|
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;
|
|
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) {
|
|
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, 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(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.md = md;
|
|
|
|
rv = md_store_iter(find_acct, &ctx, store, p, group, name_pattern, MD_FN_ACCOUNT, MD_SV_JSON);
|
|
if (ctx.id) {
|
|
*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");
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
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)
|
|
{
|
|
md_acme_acct_t *acct;
|
|
md_pkey_t *pkey;
|
|
const char *id;
|
|
apr_status_t rv;
|
|
|
|
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);
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
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;
|
|
|
|
while (APR_EAGAIN == (rv = acct_find_and_verify(store, MD_SG_ACCOUNTS,
|
|
mk_acct_pattern(acme->p, acme),
|
|
acme, md, acme->p))) {
|
|
/* nop */
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
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;
|
|
|
|
memset(&ctx, 0, sizeof(ctx));
|
|
ctx.p = p;
|
|
ctx.md = md;
|
|
|
|
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 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;
|
|
|
|
/**************************************************************************************************/
|
|
/* acct update */
|
|
|
|
static apr_status_t on_init_acct_upd(md_acme_req_t *req, void *baton)
|
|
{
|
|
(void)baton;
|
|
return md_acme_req_body_init(req, NULL);
|
|
}
|
|
|
|
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 (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);
|
|
}
|
|
|
|
apr_array_clear(acct->contacts);
|
|
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;
|
|
}
|
|
|
|
apr_status_t md_acme_acct_update(md_acme_t *acme)
|
|
{
|
|
acct_ctx_t ctx;
|
|
|
|
md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "acct update");
|
|
if (!acme->acct) {
|
|
return APR_EINVAL;
|
|
}
|
|
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);
|
|
}
|
|
|
|
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_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_acct_save(store, p, acme, &acme->acct_id, acme->acct, acme->acct_key);
|
|
}
|
|
}
|
|
acme->acct = NULL;
|
|
acme->acct_key = NULL;
|
|
rv = APR_ENOENT;
|
|
}
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
/**************************************************************************************************/
|
|
/* 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_json_t *eab, *prot_fields, *jwk;
|
|
md_data_t payload, hmac_key;
|
|
apr_status_t rv;
|
|
|
|
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;
|
|
}
|
|
payload.len = strlen(payload.data);
|
|
|
|
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;
|
|
}
|
|
|
|
static apr_status_t on_init_acct_new(md_acme_req_t *req, void *baton)
|
|
{
|
|
acct_ctx_t *ctx = baton;
|
|
md_json_t *jpayload, *jeab;
|
|
apr_status_t rv;
|
|
|
|
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_acct_register(md_acme_t *acme, md_store_t *store,
|
|
const md_t *md, apr_pool_t *p)
|
|
{
|
|
apr_status_t rv;
|
|
md_pkey_t *pkey;
|
|
const char *err = NULL, *uri;
|
|
md_pkey_spec_t spec;
|
|
int i;
|
|
acct_ctx_t ctx;
|
|
|
|
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;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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_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("deactivated", jpayload, MD_KEY_STATUS, NULL);
|
|
return md_acme_req_body_init(req, jpayload);
|
|
}
|
|
|
|
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) {
|
|
return APR_EINVAL;
|
|
}
|
|
md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "delete account %s from %s",
|
|
acct->url, acct->ca_url);
|
|
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);
|
|
}
|
|
|
|
/**************************************************************************************************/
|
|
/* terms-of-service */
|
|
|
|
static apr_status_t on_init_agree_tos(md_acme_req_t *req, void *baton)
|
|
{
|
|
acct_ctx_t *ctx = baton;
|
|
md_json_t *jpayload;
|
|
|
|
jpayload = md_json_create(req->p);
|
|
if (ctx->acme->acct->agreement) {
|
|
md_json_setb(1, jpayload, "termsOfServiceAgreed", NULL);
|
|
}
|
|
return md_acme_req_body_init(req, jpayload);
|
|
}
|
|
|
|
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, NULL, &ctx);
|
|
}
|
|
|
|
apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p,
|
|
const char *agreement, const char **prequired)
|
|
{
|
|
apr_status_t rv = APR_SUCCESS;
|
|
|
|
/* 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 (!acme->acct->agreement && acme->ca_agreement) {
|
|
if (agreement) {
|
|
rv = md_acme_agree(acme, p, acme->ca_agreement);
|
|
}
|
|
else {
|
|
*prequired = acme->ca_agreement;
|
|
rv = APR_INCOMPLETE;
|
|
}
|
|
}
|
|
return rv;
|
|
}
|