mirror of
https://github.com/haproxy/haproxy.git
synced 2026-05-27 11:52:34 -04:00
7 ACME state handlers iterate over hc->res.hdrs, but they can be called after an error was detected, and the HTTP client will leave res.hdrs NULL on connection errors before headers are received. Let's check this inside the loop, like the chkorder handler already does. Most of them, if not all, need to be backported to 3.2.
3679 lines
104 KiB
C
3679 lines
104 KiB
C
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/*
|
|
* Implements the ACMEv2 RFC 8555 protocol
|
|
* Implements the following extensions to the protocol:
|
|
* draft-ietf-acme-dns-persist - DNS-PERSIST-01 challenge
|
|
* draft-ietf-acme-profiles - Profiles Extension
|
|
*/
|
|
|
|
#include "haproxy/ticks.h"
|
|
#include <stddef.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include <import/ebsttree.h>
|
|
#include <import/mjson.h>
|
|
|
|
#include <haproxy/acme-t.h>
|
|
|
|
#include <haproxy/acme_resolvers.h>
|
|
#include <haproxy/base64.h>
|
|
#include <haproxy/intops.h>
|
|
#include <haproxy/cfgparse.h>
|
|
#include <haproxy/cli.h>
|
|
#include <haproxy/errors.h>
|
|
#include <haproxy/http_client.h>
|
|
#include <haproxy/jws.h>
|
|
#include <haproxy/list.h>
|
|
#include <haproxy/log.h>
|
|
#include <haproxy/pattern.h>
|
|
#include <haproxy/resolvers.h>
|
|
#include <haproxy/sink.h>
|
|
#include <haproxy/ssl_ckch.h>
|
|
#include <haproxy/ssl_gencert.h>
|
|
#include <haproxy/ssl_sock.h>
|
|
#include <haproxy/ssl_utils.h>
|
|
#include <haproxy/tools.h>
|
|
#include <haproxy/trace.h>
|
|
|
|
#define TRACE_SOURCE &trace_acme
|
|
|
|
#if defined(HAVE_ACME)
|
|
|
|
static EVP_PKEY *tmp_pkey = NULL;
|
|
static X509 *tmp_x509 = NULL;
|
|
|
|
|
|
static void acme_trace(enum trace_level level, uint64_t mask, const struct trace_source *src,
|
|
const struct ist where, const struct ist func,
|
|
const void *a1, const void *a2, const void *a3, const void *a4);
|
|
|
|
static const struct trace_event acme_trace_events[] = {
|
|
{ .mask = ACME_EV_SCHED, .name = "acme_sched", .desc = "Wakeup scheduled ACME task" },
|
|
{ .mask = ACME_EV_NEW, .name = "acme_new", .desc = "New ACME task" },
|
|
{ .mask = ACME_EV_TASK, .name = "acme_task", .desc = "ACME task" },
|
|
{ }
|
|
};
|
|
|
|
|
|
static const struct name_desc acme_trace_lockon_args[4] = {
|
|
/* arg1 */ { .name="acme_ctx", .desc="ACME context" },
|
|
/* arg2 */ { },
|
|
/* arg3 */ { },
|
|
/* arg4 */ { }
|
|
};
|
|
|
|
static const struct name_desc acme_trace_decoding[] = {
|
|
{ .name="clean", .desc="only user-friendly stuff, generally suitable for level \"user\"" },
|
|
{ .name="minimal", .desc="report only conn, no real decoding" },
|
|
{ .name="simple", .desc="add error messages" },
|
|
{ .name="advanced", .desc="add handshake-related details" },
|
|
{ .name="complete", .desc="add full data dump when available" },
|
|
{ /* end */ }
|
|
};
|
|
|
|
|
|
struct trace_source trace_acme = {
|
|
.name = IST("acme"),
|
|
.desc = "ACME",
|
|
.arg_def = TRC_ARG_PRIV,
|
|
.default_cb = acme_trace,
|
|
.known_events = acme_trace_events,
|
|
.lockon_args = acme_trace_lockon_args,
|
|
.decoding = acme_trace_decoding,
|
|
.report_events = ~0, /* report everything by default */
|
|
};
|
|
|
|
INITCALL1(STG_REGISTER, trace_register_source, &trace_acme);
|
|
|
|
static void acme_trace(enum trace_level level, uint64_t mask, const struct trace_source *src,
|
|
const struct ist where, const struct ist func,
|
|
const void *a1, const void *a2, const void *a3, const void *a4)
|
|
{
|
|
const struct acme_ctx *ctx = a1;
|
|
|
|
if (src->verbosity <= ACME_VERB_CLEAN)
|
|
return;
|
|
|
|
chunk_appendf(&trace_buf, " :");
|
|
|
|
if (mask >= ACME_EV_NEW)
|
|
chunk_appendf(&trace_buf, " acme_ctx=%p", ctx);
|
|
|
|
|
|
if (mask == ACME_EV_NEW)
|
|
chunk_appendf(&trace_buf, ", crt=%s", ctx->store->path);
|
|
|
|
if (mask >= ACME_EV_TASK) {
|
|
|
|
switch (ctx->http_state) {
|
|
case ACME_HTTP_REQ:
|
|
chunk_appendf(&trace_buf, ", http_st: ACME_HTTP_REQ");
|
|
break;
|
|
case ACME_HTTP_RES:
|
|
chunk_appendf(&trace_buf, ", http_st: ACME_HTTP_RES");
|
|
break;
|
|
}
|
|
chunk_appendf(&trace_buf, ", st: ");
|
|
switch (ctx->state) {
|
|
case ACME_RESOURCES: chunk_appendf(&trace_buf, "ACME_RESOURCES"); break;
|
|
case ACME_NEWNONCE: chunk_appendf(&trace_buf, "ACME_NEWNONCE"); break;
|
|
case ACME_CHKACCOUNT: chunk_appendf(&trace_buf, "ACME_CHKACCOUNT"); break;
|
|
case ACME_NEWACCOUNT: chunk_appendf(&trace_buf, "ACME_NEWACCOUNT"); break;
|
|
case ACME_NEWORDER: chunk_appendf(&trace_buf, "ACME_NEWORDER"); break;
|
|
case ACME_AUTH: chunk_appendf(&trace_buf, "ACME_AUTH"); break;
|
|
case ACME_INITIAL_RSLV_TRIGGER: chunk_appendf(&trace_buf, "ACME_INITIAL_RSLV_TRIGGER"); break;
|
|
case ACME_INITIAL_RSLV_READY: chunk_appendf(&trace_buf, "ACME_INITIAL_RSLV_READY"); break;
|
|
case ACME_CLI_WAIT : chunk_appendf(&trace_buf, "ACME_CLI_WAIT"); break;
|
|
case ACME_INITIAL_DELAY: chunk_appendf(&trace_buf, "ACME_INITIAL_DELAY"); break;
|
|
case ACME_RSLV_RETRY_DELAY: chunk_appendf(&trace_buf, "ACME_RSLV_RETRY_DELAY"); break;
|
|
case ACME_RSLV_TRIGGER: chunk_appendf(&trace_buf, "ACME_RSLV_TRIGGER"); break;
|
|
case ACME_RSLV_READY: chunk_appendf(&trace_buf, "ACME_RSLV_READY"); break;
|
|
case ACME_CHALLENGE: chunk_appendf(&trace_buf, "ACME_CHALLENGE"); break;
|
|
case ACME_CHKCHALLENGE: chunk_appendf(&trace_buf, "ACME_CHKCHALLENGE"); break;
|
|
case ACME_FINALIZE: chunk_appendf(&trace_buf, "ACME_FINALIZE"); break;
|
|
case ACME_CHKORDER: chunk_appendf(&trace_buf, "ACME_CHKORDER"); break;
|
|
case ACME_CERTIFICATE: chunk_appendf(&trace_buf, "ACME_CERTIFICATE"); break;
|
|
case ACME_END: chunk_appendf(&trace_buf, "ACME_END"); break;
|
|
}
|
|
}
|
|
if (mask & (ACME_EV_REQ|ACME_EV_RES)) {
|
|
const struct ist *url = a2;
|
|
const struct buffer *buf = a3;
|
|
|
|
if (mask & ACME_EV_REQ)
|
|
chunk_appendf(&trace_buf, " url: %.*s", (int)url->len, url->ptr);
|
|
|
|
if (src->verbosity >= ACME_VERB_COMPLETE && level >= TRACE_LEVEL_DATA) {
|
|
chunk_appendf(&trace_buf, " Buffer Dump:\n");
|
|
chunk_appendf(&trace_buf, "%.*s", (int)buf->data, buf->area);
|
|
}
|
|
}
|
|
}
|
|
|
|
struct eb_root acme_tasks = EB_ROOT_UNIQUE;
|
|
__decl_thread(HA_RWLOCK_T acme_lock);
|
|
|
|
static struct acme_cfg *acme_cfgs = NULL;
|
|
static struct acme_cfg *cur_acme = NULL;
|
|
|
|
static struct proxy *httpclient_acme_px = NULL;
|
|
|
|
enum acme_ret {
|
|
ACME_RET_OK = 0,
|
|
ACME_RET_RETRY = 1,
|
|
ACME_RET_FAIL = 2
|
|
};
|
|
|
|
static int acme_start_task(struct ckch_store *store, char **errmsg);
|
|
static struct task *acme_scheduler(struct task *task, void *context, unsigned int state);
|
|
|
|
/* Return an existing acme_cfg section */
|
|
struct acme_cfg *get_acme_cfg(const char *name)
|
|
{
|
|
struct acme_cfg *tmp_acme = acme_cfgs;
|
|
|
|
/* first check if the ID was already used */
|
|
while (tmp_acme) {
|
|
if (strcmp(tmp_acme->name, name) == 0)
|
|
return tmp_acme;
|
|
|
|
tmp_acme = tmp_acme->next;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Return an existing section or create one and return it */
|
|
struct acme_cfg *new_acme_cfg(const char *name)
|
|
{
|
|
struct acme_cfg *ret = NULL;
|
|
|
|
/* first check if the ID was already used. return it if that's the case */
|
|
if ((ret = get_acme_cfg(name)) != NULL)
|
|
goto out;
|
|
|
|
/* If there wasn't any section with this name, just create one */
|
|
ret = calloc(1, sizeof(*ret));
|
|
if (!ret)
|
|
return NULL;
|
|
|
|
ret->name = strdup(name);
|
|
/* 0 on the linenum just mean it was not initialized yet */
|
|
ret->linenum = 0;
|
|
|
|
ret->challenge = strdup("http-01"); /* default value */
|
|
ret->dns_delay = 30; /* default DNS re-trigger delay in seconds */
|
|
ret->dns_timeout = 600; /* default DNS retry timeout */
|
|
|
|
/* The default generated keys are EC-384 */
|
|
ret->key.type = EVP_PKEY_EC;
|
|
ret->key.curves = NID_secp384r1;
|
|
|
|
/* default to 2048 bits when using RSA */
|
|
ret->key.bits = 2048;
|
|
|
|
/* HS256 is the only sane choice for HMAC */
|
|
ret->eab.mac_alg = JWS_ALG_HS256;
|
|
|
|
ret->next = acme_cfgs;
|
|
acme_cfgs = ret;
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* ckch_conf acme parser
|
|
*/
|
|
int ckch_conf_acme_init(void *value, char *buf, struct ckch_store *s, int cli, const char *filename, int linenum, char **err)
|
|
{
|
|
int err_code = 0;
|
|
struct acme_cfg *cfg;
|
|
|
|
cfg = new_acme_cfg(value);
|
|
if (!cfg) {
|
|
memprintf(err, "out of memory.\n");
|
|
err_code |= ERR_FATAL| ERR_ALERT;
|
|
goto error;
|
|
}
|
|
|
|
if (cfg->linenum == 0) {
|
|
if (filename)
|
|
cfg->filename = strdup(filename);
|
|
/* store the linenum as a negative value because is the one of
|
|
* the crt-store, not the one of the section. It will be replace
|
|
* by the one of the section once initialized
|
|
*/
|
|
cfg->linenum = -linenum;
|
|
}
|
|
|
|
error:
|
|
return err_code;
|
|
}
|
|
|
|
/* Initialize the proxy for the ACME HTTP client */
|
|
static int httpclient_acme_init()
|
|
{
|
|
httpclient_acme_px = httpclient_create_proxy("<ACME>");
|
|
if (!httpclient_acme_px)
|
|
return ERR_FATAL;
|
|
httpclient_acme_px->logformat.str = httpsclient_log_format; /* ACME server are always SSL */
|
|
|
|
return ERR_NONE;
|
|
}
|
|
|
|
|
|
/* acme section parser
|
|
* Fill the acme_cfgs linked list
|
|
*/
|
|
static int cfg_parse_acme(const char *file, int linenum, char **args, int kwm)
|
|
{
|
|
struct cfg_kw_list *kwl;
|
|
const char *best;
|
|
int index;
|
|
int rc = 0;
|
|
int err_code = 0;
|
|
char *errmsg = NULL;
|
|
|
|
if (!experimental_directives_allowed) {
|
|
ha_alert("parsing [%s:%d]: section '%s' is experimental, must be allowed via a global 'expose-experimental-directives'\n", file, linenum, cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
mark_tainted(TAINTED_CONFIG_EXP_KW_DECLARED);
|
|
|
|
if (strcmp(args[0], "acme") == 0) {
|
|
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
if (!*args[1]) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: section '%s' requires an ID argument.\n", file, linenum, cursection);
|
|
goto out;
|
|
}
|
|
|
|
cur_acme = new_acme_cfg(args[1]);
|
|
if (!cur_acme) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
|
|
goto out;
|
|
}
|
|
|
|
|
|
/* first check if the ID was already used */
|
|
if (cur_acme->linenum > 0) {
|
|
/* an uninitialized section is created when parsing the "acme" keyword in a crt-store, with a
|
|
* linenum <= 0, however, when the linenum > 0, it means we already created a section with this
|
|
* name */
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: acme section '%s' already exists (%s:%d).\n",
|
|
file, linenum, args[1], cur_acme->filename, cur_acme->linenum);
|
|
goto out;
|
|
}
|
|
|
|
ha_free(&cur_acme->filename);
|
|
cur_acme->filename = strdup(file);
|
|
cur_acme->linenum = linenum;
|
|
|
|
goto out;
|
|
}
|
|
|
|
list_for_each_entry(kwl, &cfg_keywords.list, list) {
|
|
for (index = 0; kwl->kw[index].kw != NULL; index++) {
|
|
if (kwl->kw[index].section != CFG_ACME)
|
|
continue;
|
|
if (strcmp(kwl->kw[index].kw, args[0]) == 0) {
|
|
if (check_kw_experimental(&kwl->kw[index], file, linenum, &errmsg)) {
|
|
ha_alert("%s\n", errmsg);
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
|
|
/* prepare error message just in case */
|
|
rc = kwl->kw[index].parse(args, CFG_ACME, NULL, NULL, file, linenum, &errmsg);
|
|
if (rc & ERR_ALERT) {
|
|
ha_alert("parsing [%s:%d] : %s\n", file, linenum, errmsg);
|
|
err_code |= rc;
|
|
goto out;
|
|
}
|
|
else if (rc & ERR_WARN) {
|
|
ha_warning("parsing [%s:%d] : %s\n", file, linenum, errmsg);
|
|
err_code |= rc;
|
|
goto out;
|
|
}
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
|
|
best = cfg_find_best_match(args[0], &cfg_keywords.list, CFG_ACME, NULL);
|
|
if (best)
|
|
ha_alert("parsing [%s:%d] : unknown keyword '%s' in '%s' section; did you mean '%s' maybe ?\n", file, linenum, args[0], cursection, best);
|
|
else
|
|
ha_alert("parsing [%s:%d] : unknown keyword '%s' in '%s' section\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
|
|
out:
|
|
if (err_code & ERR_FATAL)
|
|
err_code |= ERR_ABORT;
|
|
free(errmsg);
|
|
return err_code;
|
|
|
|
|
|
}
|
|
|
|
static int cfg_parse_acme_kws(char **args, int section_type, struct proxy *curpx, const struct proxy *defpx,
|
|
const char *file, int linenum, char **err)
|
|
{
|
|
int err_code = 0;
|
|
char *errmsg = NULL;
|
|
|
|
if (strcmp(args[0], "directory") == 0) {
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
ha_free(&cur_acme->directory);
|
|
cur_acme->directory = strdup(args[1]);
|
|
if (!cur_acme->directory) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
|
|
goto out;
|
|
}
|
|
} else if (strcmp(args[0], "contact") == 0) {
|
|
/* save the contact email */
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
ha_free(&cur_acme->account.contact);
|
|
cur_acme->account.contact = strdup(args[1]);
|
|
if (!cur_acme->account.contact) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
|
|
goto out;
|
|
}
|
|
} else if (strcmp(args[0], "account-key") == 0) {
|
|
/* save the filename of the account key */
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires a filename argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
ha_free(&cur_acme->account.file);
|
|
cur_acme->account.file = strdup(args[1]);
|
|
if (!cur_acme->account.file) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
|
|
goto out;
|
|
}
|
|
} else if (strcmp(args[0], "eab-key-id") == 0) {
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
ha_free(&cur_acme->eab.kid_file);
|
|
cur_acme->eab.kid_file = strdup(args[1]);
|
|
if (!cur_acme->eab.kid_file) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
|
|
goto out;
|
|
}
|
|
} else if (strcmp(args[0], "eab-mac-key") == 0) {
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
ha_free(&cur_acme->eab.mac_key_file);
|
|
cur_acme->eab.mac_key_file = strdup(args[1]);
|
|
if (!cur_acme->eab.mac_key_file) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
|
|
goto out;
|
|
}
|
|
} else if (strcmp(args[0], "eab-mac-alg") == 0) {
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
if (strcmp(args[1], "HS256") == 0) {
|
|
cur_acme->eab.mac_alg = JWS_ALG_HS256;
|
|
} else if (strcmp(args[1], "HS384") == 0) {
|
|
cur_acme->eab.mac_alg = JWS_ALG_HS384;
|
|
} else if (strcmp(args[1], "HS512") == 0) {
|
|
cur_acme->eab.mac_alg = JWS_ALG_HS512;
|
|
} else {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' must be one of the following: HS256, HS384, HS512\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
} else if (strcmp(args[0], "challenge") == 0) {
|
|
if ((!*args[1]) ||
|
|
((strcasecmp("http-01", args[1]) != 0) &&
|
|
(strcasecmp("dns-01", args[1]) != 0) &&
|
|
(strcasecmp("dns-persist-01", args[1]) != 0))) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' must be one of the following: http-01, dns-01, dns-persist-01\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
ha_free(&cur_acme->challenge);
|
|
cur_acme->challenge = strdup(args[1]);
|
|
if (!cur_acme->challenge) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
|
|
goto out;
|
|
}
|
|
|
|
/* require the CLI by default */
|
|
if ((strcasecmp("dns-01", args[1]) == 0) && (cur_acme->cond_ready == 0)) {
|
|
cur_acme->cond_ready = ACME_RDY_CLI;
|
|
}
|
|
|
|
/* dns-persist-01: wait then check for DNS propagation by default */
|
|
if ((strcasecmp("dns-persist-01", args[1]) == 0) && (cur_acme->cond_ready == 0)) {
|
|
cur_acme->cond_ready = ACME_RDY_DNS | ACME_RDY_DELAY;
|
|
}
|
|
|
|
if ((strcasecmp("http-01", args[1]) == 0) && (cur_acme->cond_ready != 0)) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section, \"http-01\" is not compatible with the \"challenge-ready\" option\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
|
|
} else if (strcmp(args[0], "profile") == 0) {
|
|
/* save the profile name */
|
|
const char *p;
|
|
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
/* profile names are used verbatim in a JSON string; only allow
|
|
* alphanumeric characters, hyphens and underscores */
|
|
for (p = args[1]; *p; p++) {
|
|
if (!isalnum((uchar)*p) && *p != '-' && *p != '_') {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section contains unauthorized character '%c'\n", file, linenum, args[0], cursection, *p);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
ha_free(&cur_acme->profile);
|
|
cur_acme->profile = strdup(args[1]);
|
|
if (!cur_acme->profile) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
|
|
goto out;
|
|
}
|
|
} else if (strcmp(args[0], "map") == 0) {
|
|
/* save the map name for thumbprint + token storage */
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
ha_free(&cur_acme->map);
|
|
cur_acme->map = strdup(args[1]);
|
|
if (!cur_acme->map) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
|
|
goto out;
|
|
}
|
|
} else if (strcmp(args[0], "challenge-ready") == 0) {
|
|
char *str = args[1];
|
|
char *saveptr;
|
|
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
cur_acme->cond_ready = 0;
|
|
|
|
while ((str = strtok_r(str, ",", &saveptr))) {
|
|
|
|
if (strcmp(str, "cli") == 0) {
|
|
/* wait for the CLI-ready to run the challenge */
|
|
cur_acme->cond_ready |= ACME_RDY_CLI;
|
|
} else if (strcmp(str, "dns") == 0) {
|
|
/* wait for the DNS-check to run the challenge */
|
|
cur_acme->cond_ready |= ACME_RDY_DNS;
|
|
} else if (strcmp(str, "delay") == 0) {
|
|
/* wait for the DNS-check to run the challenge */
|
|
cur_acme->cond_ready |= ACME_RDY_DELAY;
|
|
} else if (strcmp(str, "none") == 0) {
|
|
if (cur_acme->cond_ready || (saveptr && *saveptr)) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' can't combine 'none' with other keywords.\n", file, linenum, args[0], cursection);
|
|
goto out;
|
|
}
|
|
cur_acme->cond_ready = ACME_RDY_NONE;
|
|
} else {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires parameter separated by commas: 'cli', 'dns' or 'none'\n", file, linenum, args[0], cursection);
|
|
goto out;
|
|
}
|
|
str = NULL;
|
|
}
|
|
|
|
if ((strcasecmp("http-01", cur_acme->challenge) == 0) && (cur_acme->cond_ready != 0)) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section, \"http-01\" is not compatible with the \"challenge-ready\" option\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
|
|
} else if (strcmp(args[0], "dns-delay") == 0) {
|
|
const char *res;
|
|
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
res = parse_time_err(args[1], &cur_acme->dns_delay, TIME_UNIT_S);
|
|
if (res == PARSE_TIME_OVER) {
|
|
ha_alert("parsing [%s:%d]: timer overflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
} else if (res == PARSE_TIME_UNDER) {
|
|
ha_alert("parsing [%s:%d]: timer underflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
} else if (res) {
|
|
ha_alert("parsing [%s:%d]: unexpected character '%c' in argument to '%s'\n", file, linenum, *res, args[0]);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
} else if (strcmp(args[0], "dns-timeout") == 0) {
|
|
const char *res;
|
|
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
res = parse_time_err(args[1], &cur_acme->dns_timeout, TIME_UNIT_S);
|
|
if (res == PARSE_TIME_OVER) {
|
|
ha_alert("parsing [%s:%d]: timer overflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
} else if (res == PARSE_TIME_UNDER) {
|
|
ha_alert("parsing [%s:%d]: timer underflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
} else if (res) {
|
|
ha_alert("parsing [%s:%d]: unexpected character '%c' in argument to '%s'\n", file, linenum, *res, args[0]);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
} else if (strcmp(args[0], "reuse-key") == 0) {
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
if (strcmp(args[1], "on") == 0) {
|
|
cur_acme->reuse_key = 1;
|
|
} else if (strcmp(args[1], "off") == 0) {
|
|
cur_acme->reuse_key = 0;
|
|
} else {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires either the 'on' or 'off' parameter\n", file, linenum, args[0], cursection);
|
|
goto out;
|
|
}
|
|
} else if (*args[0] != 0) {
|
|
ha_alert("parsing [%s:%d]: unknown keyword '%s' in '%s' section\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
out:
|
|
free(errmsg);
|
|
return err_code;
|
|
}
|
|
|
|
|
|
/* parsing "acme-provider" and "acme-vars" and add escaping of double quotes */
|
|
static int cfg_parse_acme_vars_provider(char **args, int section_type, struct proxy *curpx, const struct proxy *defpx,
|
|
const char *file, int linenum, char **err)
|
|
{
|
|
int err_code = 0;
|
|
char *errmsg = NULL;
|
|
char **dst = NULL;
|
|
char *src = args[1];
|
|
char *tmp = NULL;
|
|
int i = 0;
|
|
int len;
|
|
|
|
if (strcmp(args[0], "acme-vars") == 0) {
|
|
dst = &cur_acme->vars;
|
|
} else if (strcmp(args[0], "provider-name") == 0) {
|
|
dst = &cur_acme->provider;
|
|
} else {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: unsupported keyword '%s'.\n", file, linenum, args[0]);
|
|
goto out;
|
|
}
|
|
|
|
if (dst)
|
|
free(*dst);
|
|
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
len = strlen(src);
|
|
tmp = malloc(len + 1);
|
|
if (!tmp)
|
|
goto vars_end;
|
|
|
|
/* escape the " character */
|
|
while (*src) {
|
|
if (*src == '"') {
|
|
char *tmp2 = NULL;
|
|
|
|
len++;
|
|
tmp2 = realloc(tmp, len + 1);
|
|
if (!tmp2) {
|
|
ha_free(&tmp);
|
|
goto vars_end;
|
|
}
|
|
tmp = tmp2;
|
|
tmp[i++] = '\\'; /* add escaping */
|
|
}
|
|
tmp[i++] = *src;
|
|
src++;
|
|
}
|
|
tmp[i] = '\0';
|
|
|
|
vars_end:
|
|
*dst = tmp;
|
|
if (!*dst) {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
|
|
goto out;
|
|
}
|
|
|
|
out:
|
|
free(errmsg);
|
|
return err_code;
|
|
}
|
|
|
|
static int cfg_parse_acme_cfg_key(char **args, int section_type, struct proxy *curpx, const struct proxy *defpx,
|
|
const char *file, int linenum, char **err)
|
|
{
|
|
int err_code = 0;
|
|
char *errmsg = NULL;
|
|
|
|
if (strcmp(args[0], "keytype") == 0) {
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
if (strcmp(args[1], "RSA") == 0) {
|
|
cur_acme->key.type = EVP_PKEY_RSA;
|
|
} else if (strcmp(args[1], "ECDSA") == 0) {
|
|
cur_acme->key.type = EVP_PKEY_EC;
|
|
} else {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires either 'RSA' or 'ECDSA' argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
|
|
} else if (strcmp(args[0], "bits") == 0) {
|
|
char *stop;
|
|
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
|
|
cur_acme->key.bits = strtol(args[1], &stop, 10);
|
|
if (*stop != '\0') {
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
ha_alert("parsing [%s:%d] : cannot parse '%s' value '%s', an integer is expected.\n", file, linenum, args[0], args[1]);
|
|
goto out;
|
|
}
|
|
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
} else if (strcmp(args[0], "curves") == 0) {
|
|
if (!*args[1]) {
|
|
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto out;
|
|
|
|
if ((cur_acme->key.curves = curves2nid(args[1])) == -1) {
|
|
ha_alert("parsing [%s:%d]: unsupported curves '%s'\n", file, linenum, args[1]);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
out:
|
|
free(errmsg);
|
|
return err_code;
|
|
}
|
|
|
|
/* parse 'acme.scheduler' option */
|
|
static int cfg_parse_global_acme_sched(char **args, int section_type, struct proxy *curpx, const struct proxy *defpx,
|
|
const char *file, int linenum, char **err)
|
|
{
|
|
int err_code = 0;
|
|
|
|
if (!*args[1]) {
|
|
memprintf(err, "parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
|
|
goto error;
|
|
}
|
|
if (alertif_too_many_args(1, file, linenum, args, &err_code))
|
|
goto error;
|
|
|
|
if (strcmp(args[1], "auto") == 0) {
|
|
global_ssl.acme_scheduler = 1;
|
|
} else if (strcmp(args[1], "off") == 0) {
|
|
global_ssl.acme_scheduler = 0;
|
|
} else {
|
|
memprintf(err, "parsing [%s:%d]: keyword '%s' in '%s' section requires either 'auto' or 'off' argument", file, linenum, args[0], cursection);
|
|
goto error;
|
|
}
|
|
|
|
return 0;
|
|
|
|
error:
|
|
return -1;
|
|
}
|
|
|
|
/* Initialize stuff once the section is parsed */
|
|
static int cfg_postsection_acme()
|
|
{
|
|
struct ckch_store *store;
|
|
EVP_PKEY *key = NULL;
|
|
BIO *bio = NULL;
|
|
int err_code = 0;
|
|
char *errmsg = NULL;
|
|
char *path;
|
|
char store_path[PATH_MAX]; /* complete path with crt_base */
|
|
struct stat st;
|
|
|
|
/* if dns-persist-01 is set, add an extra INITIAL_DNS check */
|
|
if (strcasecmp(cur_acme->challenge, "dns-persist-01") == 0)
|
|
cur_acme->cond_ready |= ACME_RDY_INITIAL_DNS;
|
|
|
|
/* if account key filename is unspecified, choose a filename for it */
|
|
if (!cur_acme->account.file) {
|
|
if (!memprintf(&cur_acme->account.file, "%s.account.key", cur_acme->name)) {
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
ha_alert("acme: out of memory.\n");
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (cur_acme->eab.kid_file != NULL && cur_acme->eab.mac_key_file != NULL) {
|
|
int rv = 0;
|
|
rv = read_line_to_trash("%s", cur_acme->eab.kid_file);
|
|
if (rv >= 1) {
|
|
/* if read at least one character successfully */
|
|
const char *p;
|
|
|
|
cur_acme->eab.kid = my_strndup(trash.area, trash.data);
|
|
if (!cur_acme->eab.kid) {
|
|
ha_alert("acme: out of memory.\n");
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
|
|
/* technically ACME RFC allows any ASCII string here,
|
|
* but in practice CAs usually provide key id as a base64url encoded secret or an UUID
|
|
* this warning may need to be adjusted in the future */
|
|
for (p = cur_acme->eab.kid; *p; p++) {
|
|
if (!isalnum((uchar)*p) && *p != '-' && *p != '_') {
|
|
ha_warning("acme: section '%s': EAB key id contains strange character '%c'.\n", cur_acme->name, *p);
|
|
break; /* no need to print this warning many times */
|
|
}
|
|
}
|
|
} else if (rv == 0) {
|
|
/* empty files are allowed, but issue a log message */
|
|
ha_notice("acme: section '%s': EAB key id from '%s' is empty.\n", cur_acme->name, cur_acme->eab.kid_file);
|
|
} else {
|
|
ha_alert("acme: section '%s': couldn't load EAB key id from '%s', code %d.\n", cur_acme->name, cur_acme->eab.kid_file, rv);
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
|
|
rv = read_line_to_trash("%s", cur_acme->eab.mac_key_file);
|
|
if (rv >= 1) {
|
|
struct buffer *dec_mac = get_trash_chunk();
|
|
int bytes = 0;
|
|
int alg_bytes = 0;
|
|
|
|
bytes = base64urldec(trash.area, trash.data, dec_mac->area, dec_mac->size);
|
|
if (bytes < 0) {
|
|
ha_alert("acme: section '%s': failed to base64url decode EAB MAC key.\n", cur_acme->name);
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
dec_mac->data = bytes;
|
|
|
|
switch (cur_acme->eab.mac_alg) {
|
|
case JWS_ALG_HS256: alg_bytes = 32; break;
|
|
case JWS_ALG_HS384: alg_bytes = 48; break;
|
|
case JWS_ALG_HS512: alg_bytes = 64; break;
|
|
default:
|
|
ha_alert("acme: invalid mac alg.\n");
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
|
|
if (bytes < alg_bytes) {
|
|
ha_alert("acme: section '%s': EAB mac key from '%s' is only %d bytes long, but at least %d bytes is required for the specified mac type.\n",
|
|
cur_acme->name, cur_acme->eab.mac_key_file, bytes, alg_bytes);
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
|
|
if (chunk_dup(&cur_acme->eab.mac_key, dec_mac) == NULL) {
|
|
ha_alert("acme: out of memory.\n");
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
} else if (rv == 0) {
|
|
ha_notice("acme: section '%s': EAB MAC key from '%s' is empty.\n", cur_acme->name, cur_acme->eab.mac_key_file);
|
|
} else {
|
|
ha_alert("acme: section '%s': couldn't load EAB MAC key from '%s', code %d.\n", cur_acme->name, cur_acme->eab.mac_key_file, rv);
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
} else if ((cur_acme->eab.kid_file == NULL) != (cur_acme->eab.mac_key_file == NULL)) {
|
|
ha_alert("acme: section '%s': EAB MAC key and key id are mutually dependent, specify both or neither.\n", cur_acme->name);
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
|
|
if (global_ssl.crt_base && *cur_acme->account.file != '/') {
|
|
int rv;
|
|
/* When no crt_store name, complete the name in the ckch_tree with 'crt-base' */
|
|
|
|
rv = snprintf(store_path, sizeof(store_path), "%s/%s", global_ssl.crt_base, cur_acme->account.file);
|
|
if (rv >= sizeof(store_path)) {
|
|
ha_alert("'%s/%s' : path too long", global_ssl.crt_base, cur_acme->account.file);
|
|
err_code |= ERR_ALERT | ERR_FATAL;
|
|
goto out;
|
|
}
|
|
path = store_path;
|
|
} else {
|
|
path = cur_acme->account.file;
|
|
}
|
|
|
|
if (!cur_acme->directory) {
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
ha_alert("acme: No directory defined in ACME section '%s'.\n", cur_acme->name);
|
|
goto out;
|
|
}
|
|
|
|
store = ckch_store_new(path);
|
|
if (!store) {
|
|
ha_alert("acme: out of memory.\n");
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
|
|
/* tries to open the account key */
|
|
if (stat(path, &st) == 0) {
|
|
if (ssl_sock_load_key_into_ckch(path, NULL, store->data, &errmsg)) {
|
|
memprintf(&errmsg, "%s'%s' is present but cannot be read or parsed.\n", errmsg ? errmsg : "", path);
|
|
if (errmsg)
|
|
indent_msg(&errmsg, 8);
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
ha_alert("acme: %s\n", errmsg);
|
|
goto out;
|
|
}
|
|
/* ha_notice("acme: reading account key '%s' for id '%s'.\n", path, cur_acme->name); */
|
|
} else {
|
|
ha_notice("acme: generate account key '%s' for acme section '%s'.\n", path, cur_acme->name);
|
|
|
|
if ((key = ssl_gen_EVP_PKEY(cur_acme->key.type, cur_acme->key.curves, cur_acme->key.bits, &errmsg)) == NULL) {
|
|
ha_alert("acme: %s\n", errmsg);
|
|
goto out;
|
|
}
|
|
|
|
if ((bio = BIO_new_file(store->path, "w+")) == NULL) {
|
|
ha_alert("acme: cannot create the file '%s', check your permissions.\n", cur_acme->account.file);
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
|
|
if ((PEM_write_bio_PrivateKey(bio, key, NULL, NULL, 0, NULL, NULL)) == 0) {
|
|
ha_alert("acme: cannot write account key '%s'.\n", cur_acme->account.file);
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
|
|
store->data->key = key;
|
|
key = NULL;
|
|
}
|
|
|
|
if (store->data->key == NULL) {
|
|
ha_alert("acme: No Private Key found in '%s'.\n", path);
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
|
|
cur_acme->account.pkey = store->data->key;
|
|
EVP_PKEY_up_ref(cur_acme->account.pkey);
|
|
|
|
trash.data = jws_thumbprint(cur_acme->account.pkey, trash.area, trash.size);
|
|
|
|
cur_acme->account.thumbprint = my_strndup(trash.area, trash.data);
|
|
if (!cur_acme->account.thumbprint) {
|
|
ha_alert("acme: out of memory.\n");
|
|
err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
|
|
goto out;
|
|
}
|
|
|
|
ckch_store_free(store);
|
|
|
|
out:
|
|
EVP_PKEY_free(key);
|
|
BIO_free_all(bio);
|
|
ha_free(&errmsg);
|
|
return err_code;
|
|
}
|
|
|
|
/* initialize the httpclient just before check_config_validity() because it could create a defaults resolver if it
|
|
* doesn't exist. */
|
|
static int cfg_precheck_acme()
|
|
{
|
|
if (acme_cfgs) {
|
|
if (httpclient_acme_init() & ERR_FATAL) {
|
|
ha_alert("couldn't initialize the httpclient for ACME.\n");
|
|
return ERR_ABORT;
|
|
|
|
}
|
|
}
|
|
return ERR_NONE;
|
|
}
|
|
REGISTER_PRE_CHECK(cfg_precheck_acme);
|
|
|
|
/* postparser function checks if the ACME section was declared */
|
|
static int cfg_postparser_acme()
|
|
{
|
|
struct acme_cfg *tmp_acme = acme_cfgs;
|
|
struct task *task = NULL;
|
|
int ret = 0;
|
|
|
|
/* first check if the ID was already used */
|
|
while (tmp_acme) {
|
|
/* if the linenum is not > 0, it means the acme keyword was used without declaring a section, and the
|
|
* linenum of the crt-store is stored negatively */
|
|
if (tmp_acme->linenum <= 0) {
|
|
ret++;
|
|
ha_alert("acme '%s' was used on a crt line [%s:%d], but no '%s' section exists!\n",
|
|
tmp_acme->name, tmp_acme->filename, -tmp_acme->linenum, tmp_acme->name);
|
|
}
|
|
if (tmp_acme->map) {
|
|
struct pat_ref *ref;
|
|
|
|
ref = pat_ref_lookup(tmp_acme->map);
|
|
if (!ref) {
|
|
ret++;
|
|
ha_alert("acme section '%s' line [%s:%d] has the map '%s' configured, but this map doesn't exist\n",
|
|
tmp_acme->name, tmp_acme->filename, tmp_acme->linenum, tmp_acme->map);
|
|
}
|
|
}
|
|
tmp_acme = tmp_acme->next;
|
|
}
|
|
|
|
|
|
if (acme_cfgs && global_ssl.acme_scheduler) {
|
|
task = task_new_anywhere();
|
|
if (!task) {
|
|
ret++;
|
|
ha_alert("acme: couldn't start the scheduler!\n");
|
|
goto end;
|
|
}
|
|
task->nice = 0;
|
|
task->process = acme_scheduler;
|
|
|
|
task_wakeup(task, TASK_WOKEN_INIT);
|
|
}
|
|
|
|
end:
|
|
return ret;
|
|
}
|
|
|
|
REGISTER_CONFIG_POSTPARSER("acme", cfg_postparser_acme);
|
|
|
|
void deinit_acme()
|
|
{
|
|
struct acme_cfg *next = NULL;
|
|
|
|
while (acme_cfgs) {
|
|
|
|
next = acme_cfgs->next;
|
|
ha_free(&acme_cfgs->filename);
|
|
ha_free(&acme_cfgs->name);
|
|
ha_free(&acme_cfgs->directory);
|
|
ha_free(&acme_cfgs->account.contact);
|
|
ha_free(&acme_cfgs->account.file);
|
|
ha_free(&acme_cfgs->account.thumbprint);
|
|
EVP_PKEY_free(acme_cfgs->account.pkey);
|
|
ha_free(&acme_cfgs->vars);
|
|
ha_free(&acme_cfgs->provider);
|
|
ha_free(&acme_cfgs->challenge);
|
|
ha_free(&acme_cfgs->map);
|
|
ha_free(&acme_cfgs->profile);
|
|
ha_free(&acme_cfgs->eab.kid_file);
|
|
ha_free(&acme_cfgs->eab.mac_key_file);
|
|
chunk_destroy(&acme_cfgs->eab.mac_key);
|
|
ha_free(&acme_cfgs->eab.kid);
|
|
|
|
free(acme_cfgs);
|
|
acme_cfgs = next;
|
|
}
|
|
}
|
|
|
|
REGISTER_POST_DEINIT(deinit_acme);
|
|
|
|
static struct cfg_kw_list cfg_kws_acme = {ILH, {
|
|
{ CFG_ACME, "directory", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "contact", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "account-key", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "challenge", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "keytype", cfg_parse_acme_cfg_key },
|
|
{ CFG_ACME, "bits", cfg_parse_acme_cfg_key },
|
|
{ CFG_ACME, "curves", cfg_parse_acme_cfg_key },
|
|
{ CFG_ACME, "map", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "profile", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "reuse-key", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "challenge-ready", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "dns-delay", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "dns-timeout", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "eab-key-id", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "eab-mac-key", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "eab-mac-alg", cfg_parse_acme_kws },
|
|
{ CFG_ACME, "acme-vars", cfg_parse_acme_vars_provider },
|
|
{ CFG_ACME, "provider-name", cfg_parse_acme_vars_provider },
|
|
{ CFG_GLOBAL, "acme.scheduler", cfg_parse_global_acme_sched },
|
|
{ 0, NULL, NULL },
|
|
}};
|
|
|
|
INITCALL1(STG_REGISTER, cfg_register_keywords, &cfg_kws_acme);
|
|
|
|
REGISTER_CONFIG_SECTION("acme", cfg_parse_acme, cfg_postsection_acme);
|
|
|
|
|
|
/* free acme_ctx and its content
|
|
*
|
|
* Only acme_cfg and the httpclient is not free
|
|
*
|
|
*/
|
|
static void acme_ctx_destroy(struct acme_ctx *ctx)
|
|
{
|
|
struct acme_auth *auth;
|
|
|
|
if (!ctx)
|
|
return;
|
|
|
|
istfree(&ctx->resources.newNonce);
|
|
istfree(&ctx->resources.newAccount);
|
|
istfree(&ctx->resources.newOrder);
|
|
istfree(&ctx->nonce);
|
|
istfree(&ctx->kid);
|
|
istfree(&ctx->order);
|
|
|
|
auth = ctx->auths;
|
|
while (auth) {
|
|
struct acme_auth *next;
|
|
|
|
istfree(&auth->auth);
|
|
istfree(&auth->chall);
|
|
istfree(&auth->token);
|
|
istfree(&auth->dns);
|
|
acme_rslv_free(auth->rslv);
|
|
next = auth->next;
|
|
free(auth);
|
|
auth = next;
|
|
}
|
|
|
|
istfree(&ctx->finalize);
|
|
istfree(&ctx->certificate);
|
|
|
|
ckch_store_free(ctx->store);
|
|
|
|
X509_REQ_free(ctx->req);
|
|
|
|
|
|
free(ctx);
|
|
}
|
|
|
|
static void acme_httpclient_end(struct httpclient *hc)
|
|
{
|
|
struct task *task = hc->caller;
|
|
struct acme_ctx *ctx;
|
|
|
|
if (!task)
|
|
return;
|
|
|
|
ctx = task->context;
|
|
|
|
if (ctx->http_state == ACME_HTTP_REQ)
|
|
ctx->http_state = ACME_HTTP_RES;
|
|
|
|
task_wakeup(task, TASK_WOKEN_MSG);
|
|
}
|
|
|
|
/*
|
|
* Add a map entry with <challenge> as the key, and <thumprint> as value in the <map>.
|
|
* Return 0 upon success or 1 otherwise.
|
|
*/
|
|
static int acme_add_challenge_map(const char *map, const char *challenge, const char *thumbprint, char **errmsg)
|
|
{
|
|
int ret = 1;
|
|
struct pat_ref *ref;
|
|
struct pat_ref_elt *elt;
|
|
|
|
/* when no map configured, return without error */
|
|
if (!map)
|
|
return 0;
|
|
|
|
ref = pat_ref_lookup(map);
|
|
if (!ref) {
|
|
memprintf(errmsg, "Unknown map identifier '%s'.\n", map);
|
|
goto out;
|
|
}
|
|
|
|
HA_RWLOCK_WRLOCK(PATREF_LOCK, &ref->lock);
|
|
elt = pat_ref_load(ref, ref->curr_gen, challenge, thumbprint, -1, errmsg);
|
|
HA_RWLOCK_WRUNLOCK(PATREF_LOCK, &ref->lock);
|
|
|
|
if (elt == NULL)
|
|
goto out;
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Remove the <challenge> from the <map>
|
|
*/
|
|
static void acme_del_challenge_map(const char *map, const char *challenge)
|
|
{
|
|
struct pat_ref *ref;
|
|
|
|
/* when no map configured, return without error */
|
|
if (!map)
|
|
return;
|
|
|
|
ref = pat_ref_lookup(map);
|
|
if (!ref)
|
|
goto out;
|
|
|
|
HA_RWLOCK_WRLOCK(PATREF_LOCK, &ref->lock);
|
|
pat_ref_delete(ref, challenge);
|
|
HA_RWLOCK_WRUNLOCK(PATREF_LOCK, &ref->lock);
|
|
|
|
out:
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Remove all challenges from an acme_ctx from the <map>
|
|
*/
|
|
static void acme_del_acme_ctx_map(const struct acme_ctx *ctx)
|
|
{
|
|
struct acme_auth *auth;
|
|
|
|
/* when no map configured, return without error */
|
|
if (!ctx->cfg->map)
|
|
return;
|
|
|
|
auth = ctx->auths;
|
|
while (auth) {
|
|
acme_del_challenge_map(ctx->cfg->map, auth->token.ptr);
|
|
auth = auth->next;
|
|
}
|
|
return;
|
|
}
|
|
|
|
int acme_http_req(struct task *task, struct acme_ctx *ctx, struct ist url, enum http_meth_t meth, const struct http_hdr *hdrs, struct ist payload)
|
|
{
|
|
struct httpclient *hc;
|
|
|
|
hc = httpclient_new_from_proxy(httpclient_acme_px, task, meth, url);
|
|
if (!hc)
|
|
goto error;
|
|
|
|
if (httpclient_req_gen(hc, hc->req.url, hc->req.meth, hdrs, payload) != ERR_NONE)
|
|
goto error;
|
|
|
|
hc->ops.res_end = acme_httpclient_end;
|
|
|
|
ctx->hc = hc;
|
|
|
|
if (!httpclient_start(hc))
|
|
goto error;
|
|
|
|
return 0;
|
|
error:
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
/*
|
|
* compute a TXT record for dns-01 challenge
|
|
* base64url(sha256(token || '.' || base64url(Thumbprint(accountKey))))
|
|
*
|
|
* https://datatracker.ietf.org/doc/html/rfc8555/#section-8.4
|
|
*
|
|
*/
|
|
unsigned int acme_txt_record(const struct ist thumbprint, const struct ist token, struct buffer *output)
|
|
{
|
|
unsigned char md[EVP_MAX_MD_SIZE];
|
|
struct buffer *tmp = NULL;
|
|
unsigned int size;
|
|
int ret = 0;
|
|
|
|
|
|
if ((tmp = alloc_trash_chunk()) == NULL)
|
|
goto out;
|
|
|
|
chunk_istcat(tmp, token);
|
|
chunk_appendf(tmp, ".");
|
|
chunk_istcat(tmp, thumbprint);
|
|
|
|
if (EVP_Digest(tmp->area, tmp->data, md, &size, EVP_sha256(), NULL) == 0)
|
|
goto out;
|
|
|
|
ret = a2base64url((const char *)md, size, output->area, output->size);
|
|
if (ret < 0)
|
|
ret = 0;
|
|
output->data = ret;
|
|
|
|
out:
|
|
free_trash_chunk(tmp);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
int acme_jws_payload(struct buffer *req, struct ist nonce, struct ist url, EVP_PKEY *pkey, struct ist kid, struct buffer *output, char **errmsg)
|
|
{
|
|
struct buffer *b64payload = NULL;
|
|
struct buffer *b64prot = NULL;
|
|
struct buffer *b64sign = NULL;
|
|
struct buffer *jwk = NULL;
|
|
enum jwt_alg alg = JWS_ALG_NONE;
|
|
int ret = 1;
|
|
|
|
|
|
b64payload = alloc_trash_chunk();
|
|
b64prot = alloc_trash_chunk();
|
|
jwk = alloc_trash_chunk();
|
|
b64sign = alloc_trash_chunk();
|
|
|
|
if (!b64payload || !b64prot || !jwk || !b64sign || !output) {
|
|
memprintf(errmsg, "out of memory");
|
|
goto error;
|
|
}
|
|
|
|
if (!isttest(kid))
|
|
jwk->data = EVP_PKEY_to_pub_jwk(pkey, jwk->area, jwk->size);
|
|
alg = EVP_PKEY_to_jws_alg(pkey);
|
|
|
|
if (alg == JWS_ALG_NONE) {
|
|
memprintf(errmsg, "couldn't chose a JWK algorithm");
|
|
goto error;
|
|
}
|
|
|
|
b64payload->data = jws_b64_payload(req->area, b64payload->area, b64payload->size);
|
|
b64prot->data = jws_b64_protected(alg, kid.ptr, jwk->area, nonce.ptr, url.ptr, b64prot->area, b64prot->size);
|
|
b64sign->data = jws_b64_signature(pkey, alg, b64prot->area, b64payload->area, b64sign->area, b64sign->size);
|
|
output->data = jws_flattened(b64prot->area, b64payload->area, b64sign->area, output->area, output->size);
|
|
|
|
if (output->data == 0)
|
|
goto error;
|
|
|
|
ret = 0;
|
|
|
|
error:
|
|
free_trash_chunk(b64sign);
|
|
free_trash_chunk(jwk);
|
|
free_trash_chunk(b64prot);
|
|
free_trash_chunk(b64payload);
|
|
|
|
|
|
return ret;
|
|
}
|
|
|
|
int acme_jws_eab_payload(struct ist url, EVP_PKEY *acc_key, struct buffer mac_key, enum jwt_alg alg, char *kid, struct buffer *output, char **errmsg)
|
|
{
|
|
struct buffer *b64payload = NULL;
|
|
struct buffer *b64prot = NULL;
|
|
struct buffer *b64sign = NULL;
|
|
struct buffer *jwk = NULL;
|
|
int ret = 1;
|
|
|
|
b64payload = alloc_trash_chunk();
|
|
b64prot = alloc_trash_chunk();
|
|
jwk = alloc_trash_chunk();
|
|
b64sign = alloc_trash_chunk();
|
|
|
|
if (!b64payload || !b64prot || !jwk || !b64sign || !output) {
|
|
memprintf(errmsg, "out of memory");
|
|
goto error;
|
|
}
|
|
|
|
jwk->data = EVP_PKEY_to_pub_jwk(acc_key, jwk->area, jwk->size);
|
|
|
|
b64payload->data = jws_b64_payload(jwk->area, b64payload->area, b64payload->size);
|
|
b64prot->data = jws_b64_protected(alg, kid, NULL, NULL, url.ptr, b64prot->area, b64prot->size);
|
|
b64sign->data = jws_b64_hmac_signature(mac_key.area, mac_key.data, alg, b64prot->area, b64payload->area, b64sign->area, b64sign->size);
|
|
output->data = jws_flattened(b64prot->area, b64payload->area, b64sign->area, output->area, output->size);
|
|
|
|
if (output->data == 0)
|
|
goto error;
|
|
|
|
ret = 0;
|
|
|
|
error:
|
|
free_trash_chunk(b64payload);
|
|
free_trash_chunk(b64prot);
|
|
free_trash_chunk(jwk);
|
|
free_trash_chunk(b64sign);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Update every certificate instances for the new store
|
|
*
|
|
* XXX: ideally this should be reentrant like in lua or the CLI.
|
|
*/
|
|
int acme_update_certificate(struct task *task, struct acme_ctx *ctx, char **errmsg)
|
|
{
|
|
int ret = 1;
|
|
struct ckch_store *old_ckchs, *new_ckchs;
|
|
struct ckch_inst *ckchi;
|
|
struct sink *dpapi;
|
|
struct ist line[3];
|
|
|
|
new_ckchs = ctx->store;
|
|
|
|
if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) {
|
|
memprintf(errmsg, "couldn't get the certificate lock!");
|
|
return ret;
|
|
}
|
|
|
|
if ((old_ckchs = ckchs_lookup(new_ckchs->path)) == NULL) {
|
|
memprintf(errmsg, "couldn't find the previous certificate to update");
|
|
goto error;
|
|
}
|
|
|
|
ckchi = LIST_ELEM(old_ckchs->ckch_inst.n, typeof(ckchi), by_ckchs);
|
|
|
|
/* walk through the old ckch_inst and creates new ckch_inst using the updated ckchs */
|
|
list_for_each_entry_from(ckchi, &old_ckchs->ckch_inst, by_ckchs) {
|
|
struct ckch_inst *new_inst;
|
|
|
|
if (ckch_inst_rebuild(new_ckchs, ckchi, &new_inst, errmsg)) {
|
|
goto error;
|
|
}
|
|
|
|
/* link the new ckch_inst to the duplicate */
|
|
LIST_APPEND(&new_ckchs->ckch_inst, &new_inst->by_ckchs);
|
|
}
|
|
|
|
/* insert everything and remove the previous objects */
|
|
ckch_store_replace(old_ckchs, new_ckchs);
|
|
|
|
send_log(NULL, LOG_NOTICE,"acme: %s: Successful update of the certificate.\n", ctx->store->path);
|
|
|
|
|
|
line[0] = ist("acme newcert ");
|
|
line[1] = ist(ctx->store->path);
|
|
line[2] = ist("\n\0");
|
|
|
|
dpapi = sink_find("dpapi");
|
|
if (dpapi)
|
|
sink_write(dpapi, LOG_HEADER_NONE, 0, line, 3);
|
|
|
|
ctx->store = NULL;
|
|
|
|
ret = 0;
|
|
|
|
error:
|
|
HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
|
|
return ret;
|
|
|
|
}
|
|
|
|
int acme_res_certificate(struct task *task, struct acme_ctx *ctx, char **errmsg)
|
|
{
|
|
struct httpclient *hc;
|
|
struct http_hdr *hdrs, *hdr;
|
|
struct buffer *t1 = NULL, *t2 = NULL;
|
|
int ret = 1;
|
|
EVP_PKEY *key = NULL;
|
|
|
|
hc = ctx->hc;
|
|
if (!hc)
|
|
goto error;
|
|
|
|
if ((t1 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((t2 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
|
|
hdrs = hc->res.hdrs;
|
|
|
|
for (hdr = hdrs; hdrs && isttest(hdr->v); hdr++) {
|
|
if (isteqi(hdr->n, ist("Replay-Nonce"))) {
|
|
istfree(&ctx->nonce);
|
|
ctx->nonce = istdup(hdr->v);
|
|
}
|
|
/* get the next retry timing */
|
|
if (isteqi(hdr->n, ist("Retry-After"))) {
|
|
ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
|
|
}
|
|
}
|
|
|
|
TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);
|
|
|
|
if (hc->res.status < 200 || hc->res.status >= 300) {
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
|
|
t1->data = ret;
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
|
|
t2->data = ret;
|
|
|
|
if (t2->data && t1->data)
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting challenge URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
|
|
else
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting challengge URL", hc->res.status);
|
|
goto error;
|
|
}
|
|
|
|
/* loading a PEM would remove the key, save it for later */
|
|
key = ctx->store->data->key;
|
|
ctx->store->data->key = NULL;
|
|
|
|
/* OpenSSL's BIO_new_mem_buf() expects a NUL-terminated string when
|
|
* passed -1. The httpclient buffer lacks this, so manually terminate
|
|
* it here to prevent an out-of-bounds heap read during PEM parsing.
|
|
*/
|
|
if (b_room(&hc->res.buf) < 1) {
|
|
memprintf(errmsg, "ACME certificate response has no room for NUL terminator");
|
|
goto error;
|
|
}
|
|
hc->res.buf.area[hc->res.buf.data] = '\0';
|
|
|
|
/* XXX: might need a function dedicated to this, which does not read a private key */
|
|
if (ssl_sock_load_pem_into_ckch(ctx->store->path, hc->res.buf.area, ctx->store->data , errmsg) != 0)
|
|
goto error;
|
|
|
|
/* restore the key */
|
|
ctx->store->data->key = key;
|
|
key = NULL;
|
|
|
|
if (acme_update_certificate(task, ctx, errmsg) != 0)
|
|
goto error;
|
|
|
|
out:
|
|
ret = 0;
|
|
|
|
error:
|
|
if (key)
|
|
ctx->store->data->key = key;
|
|
free_trash_chunk(t1);
|
|
free_trash_chunk(t2);
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
return ret;
|
|
}
|
|
|
|
int acme_res_chkorder(struct task *task, struct acme_ctx *ctx, char **errmsg)
|
|
{
|
|
struct httpclient *hc;
|
|
struct http_hdr *hdrs, *hdr;
|
|
struct buffer *t1 = NULL, *t2 = NULL;
|
|
int ret = 1;
|
|
|
|
hc = ctx->hc;
|
|
if (!hc)
|
|
goto error;
|
|
|
|
if ((t1 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((t2 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
|
|
hdrs = hc->res.hdrs;
|
|
|
|
for (hdr = hdrs; hdrs && isttest(hdr->v); hdr++) {
|
|
if (isteqi(hdr->n, ist("Replay-Nonce"))) {
|
|
istfree(&ctx->nonce);
|
|
ctx->nonce = istdup(hdr->v);
|
|
}
|
|
/* get the next retry timing */
|
|
if (isteqi(hdr->n, ist("Retry-After"))) {
|
|
ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
|
|
}
|
|
}
|
|
|
|
TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);
|
|
|
|
if (hc->res.status < 200 || hc->res.status >= 300) {
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
|
|
t1->data = ret;
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
|
|
t2->data = ret;
|
|
|
|
if (t2->data && t1->data)
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting Order URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
|
|
else
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting Order URL", hc->res.status);
|
|
goto error;
|
|
}
|
|
ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.certificate", trash.area, trash.size);
|
|
if (ret == -1) {
|
|
memprintf(errmsg, "couldn't get a the certificate URL");
|
|
goto error;
|
|
}
|
|
trash.data = ret;
|
|
ctx->certificate = istdup(ist2(trash.area, trash.data));
|
|
if (!isttest(ctx->certificate)) {
|
|
memprintf(errmsg, "out of memory");
|
|
goto error;
|
|
}
|
|
ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.status", trash.area, trash.size);
|
|
if (ret == -1) {
|
|
memprintf(errmsg, "couldn't get a the Order status");
|
|
goto error;
|
|
}
|
|
trash.data = ret;
|
|
if (strncasecmp("valid", trash.area, trash.data) != 0) {
|
|
memprintf(errmsg, "order status: %.*s", (int)trash.data, trash.area);
|
|
goto error;
|
|
};
|
|
|
|
ret = 0;
|
|
|
|
error:
|
|
free_trash_chunk(t1);
|
|
free_trash_chunk(t2);
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* Send the CSR over the Finalize URL */
|
|
int acme_req_finalize(struct task *task, struct acme_ctx *ctx, char **errmsg)
|
|
{
|
|
X509_REQ *req = ctx->req;
|
|
struct buffer *csr = NULL;
|
|
struct buffer *req_in = NULL;
|
|
struct buffer *req_out = NULL;
|
|
const struct http_hdr hdrs[] = {
|
|
{ IST("Content-Type"), IST("application/jose+json") },
|
|
{ IST_NULL, IST_NULL }
|
|
};
|
|
int ret = 1;
|
|
size_t len = 0;
|
|
unsigned char *data = NULL;
|
|
|
|
if ((csr = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((req_in = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((req_out = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
|
|
len = i2d_X509_REQ(req, &data);
|
|
if (len <= 0)
|
|
goto error;
|
|
|
|
ret = a2base64url((char *)data, len, csr->area, csr->size);
|
|
if (ret <= 0)
|
|
goto error;
|
|
csr->data = ret;
|
|
|
|
chunk_printf(req_in, "{ \"csr\": \"%.*s\" }", (int)csr->data, csr->area);
|
|
|
|
|
|
if (acme_jws_payload(req_in, ctx->nonce, ctx->finalize, ctx->cfg->account.pkey, ctx->kid, req_out, errmsg) != 0)
|
|
goto error;
|
|
|
|
if (acme_http_req(task, ctx, ctx->finalize, HTTP_METH_POST, hdrs, ist2(req_out->area, req_out->data)))
|
|
goto error;
|
|
|
|
ret = 0;
|
|
goto out;
|
|
error:
|
|
memprintf(errmsg, "couldn't request the finalize URL");
|
|
out:
|
|
OPENSSL_free(data);
|
|
free_trash_chunk(req_in);
|
|
free_trash_chunk(req_out);
|
|
free_trash_chunk(csr);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
int acme_res_finalize(struct task *task, struct acme_ctx *ctx, char **errmsg)
|
|
{
|
|
struct httpclient *hc;
|
|
struct http_hdr *hdrs, *hdr;
|
|
struct buffer *t1 = NULL, *t2 = NULL;
|
|
int ret = 1;
|
|
|
|
hc = ctx->hc;
|
|
if (!hc)
|
|
goto error;
|
|
|
|
if ((t1 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((t2 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
|
|
hdrs = hc->res.hdrs;
|
|
|
|
for (hdr = hdrs; hdrs && isttest(hdr->v); hdr++) {
|
|
if (isteqi(hdr->n, ist("Replay-Nonce"))) {
|
|
istfree(&ctx->nonce);
|
|
ctx->nonce = istdup(hdr->v);
|
|
}
|
|
/* get the next retry timing */
|
|
if (isteqi(hdr->n, ist("Retry-After"))) {
|
|
ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
|
|
}
|
|
}
|
|
|
|
TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);
|
|
|
|
if (hc->res.status < 200 || hc->res.status >= 300) {
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
|
|
t1->data = ret;
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
|
|
t2->data = ret;
|
|
|
|
if (t2->data && t1->data)
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting Finalize URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
|
|
else
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting Finalize URL", hc->res.status);
|
|
goto error;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
error:
|
|
free_trash_chunk(t1);
|
|
free_trash_chunk(t2);
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Send the READY request for the challenge
|
|
*/
|
|
int acme_req_challenge(struct task *task, struct acme_ctx *ctx, struct acme_auth *auth, char **errmsg)
|
|
{
|
|
struct buffer *req_in = NULL;
|
|
struct buffer *req_out = NULL;
|
|
const struct http_hdr hdrs[] = {
|
|
{ IST("Content-Type"), IST("application/jose+json") },
|
|
{ IST_NULL, IST_NULL }
|
|
};
|
|
int ret = 1;
|
|
|
|
if ((req_in = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((req_out = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
|
|
chunk_printf(req_in, "{}");
|
|
|
|
TRACE_DATA("REQ challenge dec", ACME_EV_REQ, ctx, &auth->chall, req_in);
|
|
|
|
if (acme_jws_payload(req_in, ctx->nonce, auth->chall, ctx->cfg->account.pkey, ctx->kid, req_out, errmsg) != 0)
|
|
goto error;
|
|
|
|
TRACE_DATA("REQ challenge enc", ACME_EV_REQ, ctx, &auth->chall, req_out);
|
|
|
|
if (acme_http_req(task, ctx, auth->chall, HTTP_METH_POST, hdrs, ist2(req_out->area, req_out->data)))
|
|
goto error;
|
|
|
|
ret = 0;
|
|
goto out;
|
|
error:
|
|
memprintf(errmsg, "couldn't generate the Challenge request");
|
|
out:
|
|
free_trash_chunk(req_in);
|
|
free_trash_chunk(req_out);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
/* parse the challenge URL response */
|
|
enum acme_ret acme_res_challenge(struct task *task, struct acme_ctx *ctx, struct acme_auth *auth, int chk, char **errmsg)
|
|
{
|
|
struct httpclient *hc;
|
|
struct http_hdr *hdrs, *hdr;
|
|
struct buffer *t1 = NULL, *t2 = NULL;
|
|
enum acme_ret ret = ACME_RET_FAIL;
|
|
int res = 0;
|
|
|
|
hc = ctx->hc;
|
|
if (!hc)
|
|
goto out;
|
|
|
|
if ((t1 = alloc_trash_chunk()) == NULL)
|
|
goto out;
|
|
if ((t2 = alloc_trash_chunk()) == NULL)
|
|
goto out;
|
|
|
|
hdrs = hc->res.hdrs;
|
|
|
|
TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);
|
|
|
|
for (hdr = hdrs; hdrs && isttest(hdr->v); hdr++) {
|
|
if (isteqi(hdr->n, ist("Replay-Nonce"))) {
|
|
istfree(&ctx->nonce);
|
|
ctx->nonce = istdup(hdr->v);
|
|
}
|
|
/* get the next retry timing */
|
|
if (isteqi(hdr->n, ist("Retry-After"))) {
|
|
ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
|
|
}
|
|
}
|
|
|
|
res = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.status", trash.area, trash.size);
|
|
if (res == -1) {
|
|
memprintf(errmsg, "waiting for the status");
|
|
ret = ACME_RET_RETRY;
|
|
goto out;
|
|
}
|
|
trash.data = res;
|
|
|
|
if (strncasecmp("pending", trash.area, trash.data) == 0 || strncasecmp("processing", trash.area, trash.data) == 0) {
|
|
if (chk) { /* during challenge chk */
|
|
memprintf(errmsg, "challenge status: %.*s", (int)trash.data, trash.area);
|
|
ret = ACME_RET_RETRY;
|
|
goto out;
|
|
} else { /* during object creation */
|
|
ret = ACME_RET_OK;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (strncasecmp("valid", trash.area, trash.data) == 0) {
|
|
ret = ACME_RET_OK;
|
|
goto out;
|
|
}
|
|
|
|
if (hc->res.status < 200 || hc->res.status >= 300 || mjson_find(hc->res.buf.area, hc->res.buf.data, "$.error", NULL, NULL) == MJSON_TOK_OBJECT) {
|
|
/* XXX: need a generic URN error parser */
|
|
if ((res = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.error.detail", t1->area, t1->size)) > -1)
|
|
t1->data = res;
|
|
if ((res = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.error.type", t2->area, t2->size)) > -1)
|
|
t2->data = res;
|
|
if (t2->data && t1->data)
|
|
memprintf(errmsg, "challenge error: \"%.*s\" (%.*s) (HTTP status code %d)", (int)t1->data, t1->area, (int)t2->data, t2->area, hc->res.status);
|
|
else
|
|
memprintf(errmsg, "challenge error: unknown (HTTP status code %d)", hc->res.status);
|
|
goto out;
|
|
}
|
|
|
|
out:
|
|
free_trash_chunk(t1);
|
|
free_trash_chunk(t2);
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* generate a POST-as-GET request */
|
|
int acme_post_as_get(struct task *task, struct acme_ctx *ctx, struct ist url, char **errmsg)
|
|
{
|
|
struct buffer *req_in = NULL;
|
|
struct buffer *req_out = NULL;
|
|
const struct http_hdr hdrs[] = {
|
|
{ IST("Content-Type"), IST("application/jose+json") },
|
|
{ IST_NULL, IST_NULL }
|
|
};
|
|
int ret = 1;
|
|
|
|
if ((req_in = alloc_trash_chunk()) == NULL)
|
|
goto error_alloc;
|
|
if ((req_out = alloc_trash_chunk()) == NULL)
|
|
goto error_alloc;
|
|
|
|
TRACE_USER("POST-as-GET ", ACME_EV_REQ, ctx, &url);
|
|
|
|
/* empty payload */
|
|
if (acme_jws_payload(req_in, ctx->nonce, url, ctx->cfg->account.pkey, ctx->kid, req_out, errmsg) != 0)
|
|
goto error_jws;
|
|
|
|
TRACE_DATA("POST-as-GET enc", ACME_EV_REQ, ctx, &url, req_out);
|
|
|
|
if (acme_http_req(task, ctx, url, HTTP_METH_POST, hdrs, ist2(req_out->area, req_out->data)))
|
|
goto error_http;
|
|
|
|
ret = 0;
|
|
|
|
goto end;
|
|
|
|
error_jws:
|
|
memprintf(errmsg, "couldn't generate the JWS token: %s", errmsg ? *errmsg : "");
|
|
goto end;
|
|
|
|
error_http:
|
|
memprintf(errmsg, "couldn't generate the http request");
|
|
goto end;
|
|
|
|
error_alloc:
|
|
memprintf(errmsg, "couldn't allocate memory");
|
|
goto end;
|
|
|
|
end:
|
|
free_trash_chunk(req_in);
|
|
free_trash_chunk(req_out);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int acme_res_auth(struct task *task, struct acme_ctx *ctx, struct acme_auth *auth, char **errmsg)
|
|
{
|
|
struct httpclient *hc;
|
|
struct http_hdr *hdrs, *hdr;
|
|
struct buffer *t1 = NULL, *t2 = NULL;
|
|
int ret = 1;
|
|
int i;
|
|
int wildcard = 0;
|
|
|
|
hc = ctx->hc;
|
|
if (!hc)
|
|
goto error;
|
|
|
|
if ((t1 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((t2 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
|
|
hdrs = hc->res.hdrs;
|
|
|
|
for (hdr = hdrs; hdrs && isttest(hdr->v); hdr++) {
|
|
if (isteqi(hdr->n, ist("Replay-Nonce"))) {
|
|
istfree(&ctx->nonce);
|
|
ctx->nonce = istdup(hdr->v);
|
|
}
|
|
/* get the next retry timing */
|
|
if (isteqi(hdr->n, ist("Retry-After"))) {
|
|
ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
|
|
}
|
|
|
|
}
|
|
TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);
|
|
|
|
if (hc->res.status < 200 || hc->res.status >= 300) {
|
|
/* XXX: need a generic URN error parser */
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
|
|
t1->data = ret;
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
|
|
t2->data = ret;
|
|
if (t2->data && t1->data)
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting Authorization URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
|
|
else
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting Authorization URL", hc->res.status);
|
|
goto error;
|
|
}
|
|
|
|
/* check and save the DNS entry */
|
|
ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.identifier.type", t1->area, t1->size);
|
|
if (ret == -1) {
|
|
memprintf(errmsg, "couldn't get a type \"dns\" from Authorization URL \"%s\"", auth->auth.ptr);
|
|
goto error;
|
|
}
|
|
t1->data = ret;
|
|
|
|
ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.identifier.value", t2->area, t2->size);
|
|
if (ret == -1) {
|
|
memprintf(errmsg, "couldn't get a type \"dns\" from Authorization URL \"%s\"", auth->auth.ptr);
|
|
goto error;
|
|
}
|
|
t2->data = ret;
|
|
|
|
mjson_get_bool(hc->res.buf.area, hc->res.buf.data, "$.wildcard", &wildcard);
|
|
|
|
auth->dns = istdup(ist2(t2->area, t2->data));
|
|
|
|
ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.status", trash.area, trash.size);
|
|
if (ret == -1) {
|
|
memprintf(errmsg, "couldn't get a \"status\" from Authorization URL \"%s\"", auth->auth.ptr);
|
|
goto error;
|
|
}
|
|
trash.data = ret;
|
|
|
|
/* if auth is already valid we need to skip solving challenges */
|
|
if (strncasecmp("valid", trash.area, trash.data) == 0) {
|
|
auth->validated = 1;
|
|
goto out;
|
|
}
|
|
|
|
/* get the multiple challenges and select the one from the configuration */
|
|
for (i = 0; ; i++) {
|
|
int ret;
|
|
char chall[] = "$.challenges[XXX]";
|
|
const char *tokptr;
|
|
int toklen;
|
|
|
|
if (snprintf(chall, sizeof(chall), "$.challenges[%d]", i) >= sizeof(chall))
|
|
goto error;
|
|
|
|
/* break the loop at the end of the challenges objects list */
|
|
if (mjson_find(hc->res.buf.area, hc->res.buf.data, chall, &tokptr, &toklen) == MJSON_TOK_INVALID)
|
|
break;
|
|
|
|
ret = mjson_get_string(tokptr, toklen, "$.type", trash.area, trash.size);
|
|
if (ret == -1) {
|
|
memprintf(errmsg, "couldn't get a challenge type in challenges[%d] from Authorization URL \"%s\"", i, auth->auth.ptr);
|
|
goto error;
|
|
}
|
|
trash.data = ret;
|
|
|
|
/* skip until this is the challenge we need */
|
|
if (strncasecmp(ctx->cfg->challenge, trash.area, trash.data) != 0)
|
|
continue;
|
|
|
|
ret = mjson_get_string(tokptr, toklen, "$.url", trash.area, trash.size);
|
|
if (ret == -1) {
|
|
memprintf(errmsg, "couldn't get a challenge URL in challenges[%d] from Authorization URL \"%s\"", i, auth->auth.ptr);
|
|
goto error;
|
|
}
|
|
trash.data = ret;
|
|
auth->chall = istdup(ist2(trash.area, trash.data));
|
|
if (!isttest(auth->chall)) {
|
|
memprintf(errmsg, "out of memory");
|
|
goto error;
|
|
}
|
|
|
|
if (strcasecmp(ctx->cfg->challenge, "dns-persist-01") != 0) {
|
|
ret = mjson_get_string(tokptr, toklen, "$.token", trash.area, trash.size);
|
|
if (ret == -1) {
|
|
memprintf(errmsg, "couldn't get a token in challenges[%d] from Authorization URL \"%s\"", i, auth->auth.ptr);
|
|
goto error;
|
|
}
|
|
trash.data = ret;
|
|
auth->token = istdup(ist2(trash.area, trash.data));
|
|
if (!isttest(auth->token)) {
|
|
memprintf(errmsg, "out of memory");
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
if (strcasecmp(ctx->cfg->challenge, "dns-persist-01") == 0) {
|
|
/* Clients MUST consider a challenge malformed if the issuer-domain-names array is empty
|
|
or if it contains more than 10 entries, and MUST reject such challenges.
|
|
https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-persist#section-3.1-2.4.4
|
|
*/
|
|
|
|
struct buffer *record_values = NULL;
|
|
int n = 0;
|
|
|
|
record_values = get_trash_chunk();
|
|
|
|
for (n = 0; ; n++) {
|
|
char dom_path[] = "$.issuer-domain-names[XXX]";
|
|
|
|
if (snprintf(dom_path, sizeof(dom_path), "$.issuer-domain-names[%d]", n) >= sizeof(dom_path))
|
|
goto error;
|
|
|
|
/* break the loop at the end of the list */
|
|
if (mjson_find(tokptr, toklen, dom_path, NULL, NULL) == MJSON_TOK_INVALID)
|
|
break;
|
|
|
|
if (n >= 10) {
|
|
memprintf(errmsg, "more than 10 entries in acme issuer-domain-names");
|
|
goto error;
|
|
}
|
|
|
|
ret = mjson_get_string(tokptr, toklen, dom_path, trash.area, trash.size);
|
|
if (ret == -1) {
|
|
memprintf(errmsg, "found values other than strings in acme issuer-domain-names");
|
|
goto error;
|
|
}
|
|
trash.data = ret;
|
|
|
|
/* collect allowed domain names for better reporting */
|
|
chunk_appendf(record_values, "%s\"%.*s; accounturi=%.*s%s\"", n == 0 ? "" : " OR ",
|
|
(int)trash.data, trash.area, (int)ctx->kid.len, ctx->kid.ptr,
|
|
wildcard ? "; policy=wildcard" : "");
|
|
}
|
|
|
|
if (n == 0) {
|
|
memprintf(errmsg, "0 entries in acme issuer-domain-names");
|
|
goto error;
|
|
}
|
|
|
|
/* TODO: currently this can log more records than required when wildcards are involved */
|
|
send_log(NULL, LOG_INFO, "acme: %s: dns-persist-01 requires to set the \"_validation-persist.%.*s\" TXT record to %.*s\n",
|
|
ctx->store->path, (int)auth->dns.len, auth->dns.ptr, (int)record_values->data, record_values->area);
|
|
}
|
|
else if (strcasecmp(ctx->cfg->challenge, "dns-01") == 0) {
|
|
struct sink *dpapi;
|
|
struct ist line[16];
|
|
int nmsg = 0;
|
|
struct buffer *dns_record = NULL;
|
|
|
|
dns_record = get_trash_chunk();
|
|
|
|
/* compute a response for the TXT entry */
|
|
if (acme_txt_record(ist(ctx->cfg->account.thumbprint), auth->token, dns_record) == 0) {
|
|
memprintf(errmsg, "couldn't compute the dns-01 challenge");
|
|
goto error;
|
|
}
|
|
|
|
/* replace the token by the TXT entry */
|
|
istfree(&auth->token);
|
|
auth->token = istdup(ist2(dns_record->area, dns_record->data));
|
|
|
|
if (ctx->cfg->cond_ready & ACME_RDY_CLI)
|
|
send_log(NULL, LOG_NOTICE,"acme: %s: dns-01 requires to set the \"_acme-challenge.%.*s\" TXT record to \"%.*s\" and use the \"acme challenge_ready %s domain %.*s\" command over the CLI\n",
|
|
ctx->store->path, (int)auth->dns.len, auth->dns.ptr, (int)auth->token.len, auth->token.ptr, ctx->store->path, (int)auth->dns.len, auth->dns.ptr);
|
|
|
|
/* dump to the "dpapi" sink */
|
|
line[nmsg++] = ist("acme deploy ");
|
|
line[nmsg++] = ist(ctx->store->path);
|
|
line[nmsg++] = ist(" thumbprint ");
|
|
line[nmsg++] = ist(ctx->cfg->account.thumbprint);
|
|
line[nmsg++] = ist("\n");
|
|
|
|
if (ctx->cfg->provider) {
|
|
line[nmsg++] = ist("provider-name \"");
|
|
line[nmsg++] = ist(ctx->cfg->provider);
|
|
line[nmsg++] = ist("\"\n");
|
|
}
|
|
if (ctx->cfg->vars) {
|
|
line[nmsg++] = ist("acme-vars \"");
|
|
line[nmsg++] = ist(ctx->cfg->vars);
|
|
line[nmsg++] = ist("\"\n");
|
|
}
|
|
if (isttest(auth->dns)) {
|
|
line[nmsg++] = ist("dns-01-record \"");
|
|
line[nmsg++] = ist2(dns_record->area, dns_record->data);
|
|
line[nmsg++] = ist("\"\n");
|
|
}
|
|
|
|
line[nmsg++] = ist2( hc->res.buf.area, hc->res.buf.data); /* dump the HTTP response */
|
|
line[nmsg++] = ist("\n\0");
|
|
|
|
dpapi = sink_find("dpapi");
|
|
if (dpapi)
|
|
sink_write(dpapi, LOG_HEADER_NONE, 0, line, nmsg);
|
|
}
|
|
else if (strcasecmp(ctx->cfg->challenge, "http-01") == 0) {
|
|
/* only useful for http-01 */
|
|
if (acme_add_challenge_map(ctx->cfg->map, auth->token.ptr, ctx->cfg->account.thumbprint, errmsg) != 0) {
|
|
memprintf(errmsg, "couldn't add the token to the '%s' map: %s", ctx->cfg->map, *errmsg);
|
|
goto error;
|
|
}
|
|
}
|
|
else {
|
|
memprintf(errmsg, "impossible acme challenge: %s", ctx->cfg->challenge);
|
|
goto error;
|
|
}
|
|
|
|
/* we only need one challenge, and iteration is only used to found the right one */
|
|
break;
|
|
}
|
|
|
|
out:
|
|
ret = 0;
|
|
|
|
error:
|
|
free_trash_chunk(t1);
|
|
free_trash_chunk(t2);
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
int acme_req_neworder(struct task *task, struct acme_ctx *ctx, char **errmsg)
|
|
{
|
|
struct buffer *req_in = NULL;
|
|
struct buffer *req_out = NULL;
|
|
const struct http_hdr hdrs[] = {
|
|
{ IST("Content-Type"), IST("application/jose+json") },
|
|
{ IST_NULL, IST_NULL }
|
|
};
|
|
int ret = 1;
|
|
int first = 1;
|
|
char **san = ctx->store->conf.acme.domains;
|
|
char **ip = ctx->store->conf.acme.ips;
|
|
|
|
if ((req_in = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((req_out = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
|
|
chunk_printf(req_in, "{ \"identifiers\": [ ");
|
|
|
|
if (!san && !ip)
|
|
goto error;
|
|
|
|
for (; san && *san; san++) {
|
|
chunk_appendf(req_in, "%s{ \"type\": \"dns\", \"value\": \"%s\" }", first ? "" : ",", *san);
|
|
first = 0;
|
|
}
|
|
for (; ip && *ip; ip++) {
|
|
chunk_appendf(req_in, "%s{ \"type\": \"ip\", \"value\": \"%s\" }", first ? "" : ",", *ip);
|
|
first = 0;
|
|
}
|
|
|
|
chunk_appendf(req_in, " ]");
|
|
if (ctx->cfg->profile)
|
|
chunk_appendf(req_in, ", \"profile\": \"%s\"", ctx->cfg->profile);
|
|
chunk_appendf(req_in, " }");
|
|
|
|
TRACE_DATA("NewOrder Decode", ACME_EV_REQ, ctx, &ctx->resources.newOrder, req_in);
|
|
|
|
|
|
if (acme_jws_payload(req_in, ctx->nonce, ctx->resources.newOrder, ctx->cfg->account.pkey, ctx->kid, req_out, errmsg) != 0)
|
|
goto error;
|
|
|
|
TRACE_DATA("NewOrder JWS ", ACME_EV_REQ, ctx, &ctx->resources.newOrder, req_out);
|
|
if (acme_http_req(task, ctx, ctx->resources.newOrder, HTTP_METH_POST, hdrs, ist2(req_out->area, req_out->data)))
|
|
goto error;
|
|
|
|
ret = 0;
|
|
goto out;
|
|
error:
|
|
memprintf(errmsg, "couldn't generate the newOrder request");
|
|
out:
|
|
free_trash_chunk(req_in);
|
|
free_trash_chunk(req_out);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
int acme_res_neworder(struct task *task, struct acme_ctx *ctx, char **errmsg)
|
|
{
|
|
struct httpclient *hc;
|
|
struct http_hdr *hdrs, *hdr;
|
|
struct buffer *t1 = NULL, *t2 = NULL;
|
|
int ret = 1;
|
|
int i;
|
|
|
|
hc = ctx->hc;
|
|
if (!hc)
|
|
goto error;
|
|
|
|
if ((t1 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((t2 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
|
|
hdrs = hc->res.hdrs;
|
|
|
|
for (hdr = hdrs; hdrs && isttest(hdr->v); hdr++) {
|
|
if (isteqi(hdr->n, ist("Replay-Nonce"))) {
|
|
istfree(&ctx->nonce);
|
|
ctx->nonce = istdup(hdr->v);
|
|
}
|
|
/* get the next retry timing */
|
|
if (isteqi(hdr->n, ist("Retry-After"))) {
|
|
ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
|
|
}
|
|
/* get the order URL */
|
|
if (isteqi(hdr->n, ist("Location"))) {
|
|
istfree(&ctx->order);
|
|
ctx->order = istdup(hdr->v);
|
|
}
|
|
}
|
|
TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);
|
|
|
|
if (hc->res.status < 200 || hc->res.status >= 300) {
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
|
|
t1->data = ret;
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
|
|
t2->data = ret;
|
|
if (t2->data && t1->data)
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting newOrder URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
|
|
else
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting newOrder URL", hc->res.status);
|
|
goto error;
|
|
}
|
|
|
|
/* if the order already has a certificate URL, the validation was
|
|
* already done: skip the auth/challenge steps entirely */
|
|
ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.certificate", trash.area, trash.size);
|
|
if (ret != -1) {
|
|
trash.data = ret;
|
|
istfree(&ctx->certificate);
|
|
ctx->certificate = istdup(ist2(trash.area, trash.data));
|
|
if (!isttest(ctx->certificate)) {
|
|
memprintf(errmsg, "out of memory");
|
|
goto error;
|
|
}
|
|
goto end;
|
|
}
|
|
|
|
if (!isttest(ctx->order)) {
|
|
memprintf(errmsg, "couldn't get an order Location during newOrder");
|
|
goto error;
|
|
}
|
|
|
|
/* get the multiple authorizations URL and tokens */
|
|
for (i = 0; ; i++) {
|
|
struct acme_auth *auth;
|
|
char url[] = "$.authorizations[XXX]";
|
|
|
|
if (snprintf(url, sizeof(url), "$.authorizations[%d]", i) >= sizeof(url)) {
|
|
memprintf(errmsg, "couldn't loop on authorizations during newOrder");
|
|
goto error;
|
|
}
|
|
|
|
ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, url, trash.area, trash.size);
|
|
if (ret == -1) /* end of the authorizations array */
|
|
break;
|
|
trash.data = ret;
|
|
|
|
if ((auth = calloc(1, sizeof(*auth))) == NULL) {
|
|
memprintf(errmsg, "out of memory");
|
|
goto error;
|
|
}
|
|
|
|
auth->auth = istdup(ist2(trash.area, trash.data));
|
|
if (!isttest(auth->auth)) {
|
|
free(auth);
|
|
memprintf(errmsg, "out of memory");
|
|
goto error;
|
|
}
|
|
|
|
auth->next = ctx->auths;
|
|
ctx->auths = auth;
|
|
ctx->next_auth = auth;
|
|
}
|
|
|
|
if (!ctx->auths) {
|
|
memprintf(errmsg, "no authorizations found in newOrder response");
|
|
goto error;
|
|
}
|
|
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.finalize", trash.area, trash.size)) <= 0) {
|
|
memprintf(errmsg, "couldn't find the finalize URL");
|
|
goto error;
|
|
}
|
|
trash.data = ret;
|
|
istfree(&ctx->finalize);
|
|
ctx->finalize = istdup(ist2(trash.area, trash.data));
|
|
if (!isttest(ctx->finalize)) {
|
|
memprintf(errmsg, "out of memory");
|
|
goto error;
|
|
}
|
|
end:
|
|
ret = 0;
|
|
|
|
error:
|
|
free_trash_chunk(t1);
|
|
free_trash_chunk(t2);
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
int acme_req_account(struct task *task, struct acme_ctx *ctx, int newaccount, char **errmsg)
|
|
{
|
|
struct buffer *req_in = NULL;
|
|
struct buffer *req_out = NULL;
|
|
struct buffer *eab_req_out = NULL;
|
|
const struct http_hdr hdrs[] = {
|
|
{ IST("Content-Type"), IST("application/jose+json") },
|
|
{ IST_NULL, IST_NULL }
|
|
};
|
|
int ret = 1;
|
|
|
|
if ((req_in = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((req_out = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((eab_req_out = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
|
|
if (newaccount) {
|
|
chunk_appendf(req_in, "{");
|
|
if (ctx->cfg->eab.mac_key.data > 0 && ctx->cfg->eab.kid != NULL) {
|
|
if (acme_jws_eab_payload(ctx->resources.newAccount, ctx->cfg->account.pkey, ctx->cfg->eab.mac_key, ctx->cfg->eab.mac_alg, ctx->cfg->eab.kid, eab_req_out, errmsg) != 0)
|
|
goto out;
|
|
chunk_appendf(req_in, "\"externalAccountBinding\": %.*s,", (int)eab_req_out->data, eab_req_out->area);
|
|
}
|
|
if (ctx->cfg->account.contact)
|
|
chunk_appendf(req_in, "\"contact\": [ \"mailto:%s\" ],", ctx->cfg->account.contact);
|
|
chunk_appendf(req_in, "\"termsOfServiceAgreed\": true");
|
|
chunk_appendf(req_in, "}");
|
|
} else
|
|
chunk_appendf(req_in, "{ \"onlyReturnExisting\": true }");
|
|
|
|
TRACE_DATA("newAccount Decoded", ACME_EV_REQ, ctx, &ctx->resources.newAccount, req_in);
|
|
|
|
if (acme_jws_payload(req_in, ctx->nonce, ctx->resources.newAccount, ctx->cfg->account.pkey, ctx->kid, req_out, errmsg) != 0)
|
|
goto error;
|
|
|
|
if (acme_http_req(task, ctx, ctx->resources.newAccount, HTTP_METH_POST, hdrs, ist2(req_out->area, req_out->data)))
|
|
goto error;
|
|
|
|
ret = 0;
|
|
goto out;
|
|
error:
|
|
memprintf(errmsg, "couldn't generate the newAccount request");
|
|
out:
|
|
free_trash_chunk(req_in);
|
|
free_trash_chunk(req_out);
|
|
free_trash_chunk(eab_req_out);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int acme_res_account(struct task *task, struct acme_ctx *ctx, int newaccount, char **errmsg)
|
|
{
|
|
struct httpclient *hc;
|
|
struct http_hdr *hdrs, *hdr;
|
|
struct buffer *t1 = NULL, *t2 = NULL;
|
|
int ret = 1;
|
|
|
|
hc = ctx->hc;
|
|
if (!hc)
|
|
goto error;
|
|
|
|
if ((t1 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
if ((t2 = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
|
|
hdrs = hc->res.hdrs;
|
|
|
|
for (hdr = hdrs; hdrs && isttest(hdr->v); hdr++) {
|
|
if (isteqi(hdr->n, ist("Location"))) {
|
|
istfree(&ctx->kid);
|
|
ctx->kid = istdup(hdr->v);
|
|
}
|
|
/* get the next retry timing */
|
|
if (isteqi(hdr->n, ist("Retry-After"))) {
|
|
ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
|
|
}
|
|
if (isteqi(hdr->n, ist("Replay-Nonce"))) {
|
|
istfree(&ctx->nonce);
|
|
ctx->nonce = istdup(hdr->v);
|
|
}
|
|
}
|
|
|
|
TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);
|
|
|
|
if (hc->res.status < 200 || hc->res.status >= 300) {
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
|
|
t1->data = ret;
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
|
|
t2->data = ret;
|
|
|
|
if (!newaccount) {
|
|
/* not an error, we only need to create a new account */
|
|
if (strcmp("urn:ietf:params:acme:error:accountDoesNotExist", t2->area) == 0)
|
|
goto out;
|
|
}
|
|
|
|
if (t2->data && t1->data)
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting Account URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
|
|
else
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting Account URL", hc->res.status);
|
|
goto error;
|
|
}
|
|
out:
|
|
ret = 0;
|
|
|
|
error:
|
|
free_trash_chunk(t1);
|
|
free_trash_chunk(t2);
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
|
|
int acme_nonce(struct task *task, struct acme_ctx *ctx, char **errmsg)
|
|
{
|
|
struct httpclient *hc;
|
|
struct http_hdr *hdrs, *hdr;
|
|
|
|
hc = ctx->hc;
|
|
if (!hc)
|
|
goto error;
|
|
|
|
if (hc->res.status < 200 || hc->res.status >= 300) {
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting Nonce URL", hc->res.status);
|
|
goto error;
|
|
}
|
|
|
|
TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);
|
|
|
|
hdrs = hc->res.hdrs;
|
|
|
|
for (hdr = hdrs; hdrs && isttest(hdr->v); hdr++) {
|
|
if (isteqi(hdr->n, ist("Replay-Nonce"))) {
|
|
istfree(&ctx->nonce);
|
|
ctx->nonce = istdup(hdr->v);
|
|
// fprintf(stderr, "Replay-Nonce: %.*s\n", (int)hdr->v.len, hdr->v.ptr);
|
|
|
|
}
|
|
}
|
|
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
return 0;
|
|
|
|
error:
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
return 1;
|
|
}
|
|
|
|
int acme_directory(struct task *task, struct acme_ctx *ctx, char **errmsg)
|
|
{
|
|
struct httpclient *hc;
|
|
int ret = 0;
|
|
|
|
hc = ctx->hc;
|
|
|
|
if (!hc)
|
|
goto error;
|
|
|
|
if (hc->res.status != 200) {
|
|
memprintf(errmsg, "invalid HTTP status code %d when getting directory URL", hc->res.status);
|
|
goto error;
|
|
}
|
|
|
|
TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);
|
|
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.newNonce", trash.area, trash.size)) <= 0) {
|
|
memprintf(errmsg, "couldn't get newNonce URL from the directory URL");
|
|
goto error;
|
|
}
|
|
ctx->resources.newNonce = istdup(ist2(trash.area, ret));
|
|
if (!isttest(ctx->resources.newNonce)) {
|
|
memprintf(errmsg, "couldn't get newNonce URL from the directory URL");
|
|
goto error;
|
|
}
|
|
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.newAccount", trash.area, trash.size)) <= 0) {
|
|
memprintf(errmsg, "couldn't get newAccount URL from the directory URL");
|
|
goto error;
|
|
}
|
|
ctx->resources.newAccount = istdup(ist2(trash.area, ret));
|
|
if (!isttest(ctx->resources.newAccount)) {
|
|
memprintf(errmsg, "couldn't get newAccount URL from the directory URL");
|
|
goto error;
|
|
}
|
|
if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.newOrder", trash.area, trash.size)) <= 0) {
|
|
memprintf(errmsg, "couldn't get newOrder URL from the directory URL");
|
|
goto error;
|
|
}
|
|
ctx->resources.newOrder = istdup(ist2(trash.area, ret));
|
|
if (!isttest(ctx->resources.newOrder)) {
|
|
memprintf(errmsg, "couldn't get newOrder URL from the directory URL");
|
|
goto error;
|
|
}
|
|
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
// fprintf(stderr, "newNonce: %s\nnewAccount: %s\nnewOrder: %s\n",
|
|
// ctx->resources.newNonce.ptr, ctx->resources.newAccount.ptr, ctx->resources.newOrder.ptr);
|
|
|
|
return 0;
|
|
|
|
error:
|
|
httpclient_destroy(hc);
|
|
ctx->hc = NULL;
|
|
|
|
istfree(&ctx->resources.newNonce);
|
|
istfree(&ctx->resources.newAccount);
|
|
istfree(&ctx->resources.newOrder);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Task for ACME processing:
|
|
* - when retrying after a failure, the task must be waked up
|
|
* - when calling a get function, the httpclient is waking up the task again
|
|
* once the data are ready or upon failure
|
|
*/
|
|
struct task *acme_process(struct task *task, void *context, unsigned int state)
|
|
{
|
|
struct acme_ctx *ctx = task->context;
|
|
enum acme_st st = ctx->state;
|
|
enum http_st http_st = ctx->http_state;
|
|
char *errmsg = NULL;
|
|
|
|
re:
|
|
TRACE_USER("ACME Task Handle", ACME_EV_TASK, ctx, &st);
|
|
|
|
switch (st) {
|
|
case ACME_RESOURCES:
|
|
if (http_st == ACME_HTTP_REQ) {
|
|
if (acme_http_req(task, ctx, ist(ctx->cfg->directory), HTTP_METH_GET, NULL, IST_NULL) != 0)
|
|
goto retry;
|
|
}
|
|
|
|
if (http_st == ACME_HTTP_RES) {
|
|
if (acme_directory(task, ctx, &errmsg) != 0) {
|
|
goto retry;
|
|
}
|
|
st = ACME_NEWNONCE;
|
|
goto nextreq;
|
|
}
|
|
break;
|
|
case ACME_NEWNONCE:
|
|
if (http_st == ACME_HTTP_REQ) {
|
|
if (acme_http_req(task, ctx, ctx->resources.newNonce, HTTP_METH_HEAD, NULL, IST_NULL) != 0)
|
|
goto retry;
|
|
}
|
|
if (http_st == ACME_HTTP_RES) {
|
|
if (acme_nonce(task, ctx, &errmsg) != 0) {
|
|
goto retry;
|
|
}
|
|
st = ACME_CHKACCOUNT;
|
|
goto nextreq;
|
|
}
|
|
|
|
break;
|
|
case ACME_CHKACCOUNT:
|
|
if (http_st == ACME_HTTP_REQ) {
|
|
if (acme_req_account(task, ctx, 0, &errmsg) != 0)
|
|
goto retry;
|
|
}
|
|
if (http_st == ACME_HTTP_RES) {
|
|
if (acme_res_account(task, ctx, 0, &errmsg) != 0) {
|
|
goto retry;
|
|
}
|
|
if (!isttest(ctx->kid))
|
|
st = ACME_NEWACCOUNT;
|
|
else
|
|
st = ACME_NEWORDER;
|
|
goto nextreq;
|
|
}
|
|
break;
|
|
case ACME_NEWACCOUNT:
|
|
if (http_st == ACME_HTTP_REQ) {
|
|
if (acme_req_account(task, ctx, 1, &errmsg) != 0)
|
|
goto retry;
|
|
}
|
|
if (http_st == ACME_HTTP_RES) {
|
|
if (acme_res_account(task, ctx, 1, &errmsg) != 0) {
|
|
goto retry;
|
|
}
|
|
st = ACME_NEWORDER;
|
|
goto nextreq;
|
|
}
|
|
|
|
|
|
break;
|
|
case ACME_NEWORDER:
|
|
if (http_st == ACME_HTTP_REQ) {
|
|
if (acme_req_neworder(task, ctx, &errmsg) != 0)
|
|
goto retry;
|
|
}
|
|
if (http_st == ACME_HTTP_RES) {
|
|
if (acme_res_neworder(task, ctx, &errmsg) != 0) {
|
|
goto retry;
|
|
}
|
|
st = isttest(ctx->certificate) ? ACME_CERTIFICATE : ACME_AUTH;
|
|
goto nextreq;
|
|
}
|
|
break;
|
|
case ACME_AUTH:
|
|
if (http_st == ACME_HTTP_REQ) {
|
|
if (acme_post_as_get(task, ctx, ctx->next_auth->auth, &errmsg) != 0)
|
|
goto retry;
|
|
}
|
|
if (http_st == ACME_HTTP_RES) {
|
|
if (acme_res_auth(task, ctx, ctx->next_auth, &errmsg) != 0) {
|
|
goto retry;
|
|
}
|
|
if ((ctx->next_auth = ctx->next_auth->next) == NULL) {
|
|
if ((strcasecmp(ctx->cfg->challenge, "dns-01") == 0 ||
|
|
strcasecmp(ctx->cfg->challenge, "dns-persist-01") == 0) &&
|
|
ctx->cfg->cond_ready)
|
|
st = ACME_INITIAL_RSLV_TRIGGER;
|
|
else
|
|
st = ACME_CHALLENGE;
|
|
ctx->next_auth = ctx->auths;
|
|
}
|
|
/* call with next auth or do the challenge step */
|
|
goto nextreq;
|
|
}
|
|
break;
|
|
case ACME_INITIAL_RSLV_TRIGGER: {
|
|
/* trigger an initial dns propagation check that will
|
|
* remove the challenge-ready requirements if valid */
|
|
struct acme_auth *auth;
|
|
int all_cond_ready = ctx->cfg->cond_ready;
|
|
|
|
/* if we don't have an initial dns propagation check, let's go to the next cond_ready */
|
|
if (!(ctx->cfg->cond_ready & ACME_RDY_INITIAL_DNS)) {
|
|
st = ACME_CLI_WAIT;
|
|
goto nextreq;
|
|
}
|
|
|
|
for (auth = ctx->auths; auth != NULL; auth = auth->next) {
|
|
all_cond_ready &= auth->ready;
|
|
}
|
|
|
|
/* if everything is ready, let's do the challenge request */
|
|
if ((all_cond_ready & ctx->cfg->cond_ready) == ctx->cfg->cond_ready) {
|
|
st = ACME_CHALLENGE;
|
|
goto nextreq;
|
|
}
|
|
|
|
for (auth = ctx->auths; auth != NULL; auth = auth->next) {
|
|
if (auth->ready == ctx->cfg->cond_ready)
|
|
continue;
|
|
|
|
HA_ATOMIC_INC(&ctx->dnstasks);
|
|
|
|
auth->rslv = acme_rslv_start(auth, &ctx->dnstasks, ctx->cfg->challenge, &errmsg);
|
|
if (!auth->rslv)
|
|
goto abort;
|
|
auth->rslv->acme_task = task;
|
|
}
|
|
st = ACME_INITIAL_RSLV_READY;
|
|
goto wait;
|
|
}
|
|
break;
|
|
case ACME_INITIAL_RSLV_READY: {
|
|
struct acme_auth *auth;
|
|
int all_ready = 1;
|
|
|
|
/* if triggered by the CLI, wait for the DNS tasks to
|
|
* finish
|
|
*/
|
|
if (HA_ATOMIC_LOAD(&ctx->dnstasks) != 0)
|
|
goto wait;
|
|
|
|
/* triggered by the latest DNS task */
|
|
for (auth = ctx->auths; auth != NULL; auth = auth->next) {
|
|
if (auth->ready == ctx->cfg->cond_ready)
|
|
continue;
|
|
if (auth->rslv->result == RSLV_STATUS_VALID) {
|
|
if (strcasecmp(ctx->cfg->challenge, "dns-persist-01") == 0) {
|
|
auth->ready |= ACME_RDY_INITIAL_DNS;
|
|
}
|
|
} else {
|
|
all_ready = 0;
|
|
}
|
|
|
|
acme_rslv_free(auth->rslv);
|
|
auth->rslv = NULL;
|
|
}
|
|
if (all_ready) {
|
|
/* opportunistic validation, don't do the
|
|
* cond_ready steps */
|
|
st = ACME_CHALLENGE;
|
|
ctx->cfg->cond_ready = ACME_RDY_INITIAL_DNS;
|
|
ctx->next_auth = ctx->auths;
|
|
goto nextreq;
|
|
}
|
|
|
|
/* opportunistic DNS check failed, try the ready_cond, remove initial dns as a condition */
|
|
ctx->cfg->cond_ready &= ~ACME_RDY_INITIAL_DNS;
|
|
st = ACME_CLI_WAIT;
|
|
goto nextreq;
|
|
}
|
|
break;
|
|
|
|
case ACME_CLI_WAIT: {
|
|
struct acme_auth *auth;
|
|
int all_cond_ready = ctx->cfg->cond_ready;
|
|
|
|
for (auth = ctx->auths; auth != NULL; auth = auth->next) {
|
|
all_cond_ready &= auth->ready;
|
|
}
|
|
|
|
/* if everything is ready, let's do the challenge request */
|
|
if ((all_cond_ready & ctx->cfg->cond_ready) == ctx->cfg->cond_ready) {
|
|
st = ACME_CHALLENGE;
|
|
goto nextreq;
|
|
}
|
|
|
|
/* if we need to wait for the CLI, let's wait */
|
|
if ((ctx->cfg->cond_ready & ACME_RDY_CLI) && !(all_cond_ready & ACME_RDY_CLI))
|
|
goto wait;
|
|
|
|
/* next step */
|
|
st = ACME_INITIAL_DELAY;
|
|
goto nextreq;
|
|
}
|
|
break;
|
|
case ACME_INITIAL_DELAY: {
|
|
struct acme_auth *auth;
|
|
int all_cond_ready = ctx->cfg->cond_ready;
|
|
|
|
for (auth = ctx->auths; auth != NULL; auth = auth->next) {
|
|
all_cond_ready &= auth->ready;
|
|
}
|
|
|
|
/* if everything is ready, let's do the challenge request */
|
|
if ((all_cond_ready & ctx->cfg->cond_ready) == ctx->cfg->cond_ready) {
|
|
st = ACME_CHALLENGE;
|
|
goto nextreq;
|
|
}
|
|
|
|
/* if we don't have an initial delay, let's trigger */
|
|
if (!(ctx->cfg->cond_ready & ACME_RDY_DELAY)) {
|
|
st = ACME_RSLV_TRIGGER;
|
|
goto nextreq;
|
|
}
|
|
|
|
for (auth = ctx->auths; auth != NULL; auth = auth->next) {
|
|
auth->ready |= ACME_RDY_DELAY;
|
|
}
|
|
|
|
/* either trigger the resolution of the challenge */
|
|
if (ctx->cfg->cond_ready & ACME_RDY_DNS)
|
|
st = ACME_RSLV_TRIGGER;
|
|
else
|
|
st = ACME_CHALLENGE;
|
|
ctx->http_state = ACME_HTTP_REQ;
|
|
ctx->state = st;
|
|
send_log(NULL, LOG_NOTICE, "acme: %s: %s: waiting %ds\n",
|
|
ctx->store->path, ctx->cfg->challenge, ctx->cfg->dns_delay);
|
|
|
|
task->expire = tick_add(now_ms, ctx->cfg->dns_delay * 1000);
|
|
return task;
|
|
}
|
|
break;
|
|
case ACME_RSLV_RETRY_DELAY: {
|
|
struct acme_auth *auth;
|
|
int all_cond_ready = ctx->cfg->cond_ready;
|
|
|
|
for (auth = ctx->auths; auth != NULL; auth = auth->next) {
|
|
all_cond_ready &= auth->ready;
|
|
}
|
|
|
|
/* if everything is ready, let's do the challenge request */
|
|
if ((all_cond_ready & ctx->cfg->cond_ready) == ctx->cfg->cond_ready) {
|
|
st = ACME_CHALLENGE;
|
|
goto nextreq;
|
|
}
|
|
|
|
/* Check if the next resolution would be triggered too
|
|
* late according to the dns_timeout and abort if
|
|
* necessary. */
|
|
if (ctx->dnsstarttime && ns_to_sec(now_ns) + ctx->cfg->dns_delay > ctx->dnsstarttime + ctx->cfg->dns_timeout) {
|
|
memprintf(&errmsg, "dns-01: Couldn't resolve the TXT records in %ds.", ctx->cfg->dns_timeout);
|
|
goto abort;
|
|
}
|
|
|
|
/* we don't need to wait, we can trigger the resolution
|
|
* after the delay */
|
|
st = ACME_RSLV_TRIGGER;
|
|
ctx->http_state = ACME_HTTP_REQ;
|
|
ctx->state = st;
|
|
send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: retrying the resolution in %ds\n",
|
|
ctx->store->path, ctx->cfg->dns_delay);
|
|
|
|
task->expire = tick_add(now_ms, ctx->cfg->dns_delay * 1000);
|
|
return task;
|
|
}
|
|
break;
|
|
case ACME_RSLV_TRIGGER: {
|
|
struct acme_auth *auth;
|
|
|
|
/* set the start time of the DNS checks so we can apply
|
|
* the timeout */
|
|
if (ctx->dnsstarttime == 0)
|
|
ctx->dnsstarttime = ns_to_sec(now_ns);
|
|
/* on timer expiry, re-trigger resolution for non-ready auths */
|
|
for (auth = ctx->auths; auth != NULL; auth = auth->next) {
|
|
if (auth->ready == ctx->cfg->cond_ready)
|
|
continue;
|
|
|
|
HA_ATOMIC_INC(&ctx->dnstasks);
|
|
|
|
auth->rslv = acme_rslv_start(auth, &ctx->dnstasks, ctx->cfg->challenge, &errmsg);
|
|
if (!auth->rslv)
|
|
goto abort;
|
|
auth->rslv->acme_task = task;
|
|
}
|
|
st = ACME_RSLV_READY;
|
|
goto wait;
|
|
}
|
|
break;
|
|
case ACME_RSLV_READY: {
|
|
struct acme_auth *auth;
|
|
int all_ready = 1;
|
|
|
|
/* if triggered by the CLI, wait for the DNS tasks to
|
|
* finish
|
|
*/
|
|
if (HA_ATOMIC_LOAD(&ctx->dnstasks) != 0)
|
|
goto wait;
|
|
|
|
/* triggered by the latest DNS task */
|
|
for (auth = ctx->auths; auth != NULL; auth = auth->next) {
|
|
if (auth->ready == ctx->cfg->cond_ready)
|
|
continue;
|
|
/* for dns-01, verify the TXT record content matches the
|
|
* expected token. for dns-persist-01, only check that
|
|
* the record exists since the resolver cannot read
|
|
* multiple strings within a single TXT entry */
|
|
if (auth->rslv->result == RSLV_STATUS_VALID) {
|
|
if (strcasecmp(ctx->cfg->challenge, "dns-01") == 0) {
|
|
if (isteq(auth->rslv->txt, auth->token)) {
|
|
auth->ready |= ACME_RDY_DNS;
|
|
} else {
|
|
send_log(NULL, LOG_NOTICE,
|
|
"acme: %s: dns-01: TXT record mismatch for \"_acme-challenge.%.*s\": expected \"%.*s\", got \"%.*s\"\n",
|
|
ctx->store->path, (int)auth->dns.len, auth->dns.ptr,
|
|
(int)auth->token.len, auth->token.ptr,
|
|
(int)auth->rslv->txt.len, auth->rslv->txt.ptr);
|
|
all_ready = 0;
|
|
}
|
|
} else if (strcasecmp(ctx->cfg->challenge, "dns-persist-01") == 0) {
|
|
auth->ready |= ACME_RDY_DNS;
|
|
}
|
|
} else {
|
|
send_log(NULL, LOG_NOTICE, "acme: %s: %s: Couldn't get the TXT record for \"%s.%.*s\" (status=%d)\n",
|
|
ctx->store->path, ctx->cfg->challenge,
|
|
strcasecmp(ctx->cfg->challenge, "dns-persist-01") == 0 ? "_validation-persist" : "_acme-challenge",
|
|
(int)auth->dns.len, auth->dns.ptr,
|
|
auth->rslv->result);
|
|
all_ready = 0;
|
|
}
|
|
acme_rslv_free(auth->rslv);
|
|
auth->rslv = NULL;
|
|
}
|
|
if (all_ready) {
|
|
st = ACME_CHALLENGE;
|
|
ctx->next_auth = ctx->auths;
|
|
goto nextreq;
|
|
}
|
|
|
|
/* not all ready yet, retry after dns-delay */
|
|
st = ACME_RSLV_RETRY_DELAY;
|
|
goto nextreq;
|
|
}
|
|
break;
|
|
case ACME_CHALLENGE:
|
|
if (http_st == ACME_HTTP_REQ) {
|
|
/* if challenge is already validated we skip this stage */
|
|
if (ctx->next_auth->validated) {
|
|
if ((ctx->next_auth = ctx->next_auth->next) == NULL) {
|
|
st = ACME_CHKCHALLENGE;
|
|
ctx->next_auth = ctx->auths;
|
|
}
|
|
goto nextreq;
|
|
}
|
|
|
|
/* if the challenge is not ready, wait to be woken up */
|
|
if (ctx->next_auth->ready != ctx->cfg->cond_ready)
|
|
goto wait;
|
|
|
|
if (acme_req_challenge(task, ctx, ctx->next_auth, &errmsg) != 0)
|
|
goto retry;
|
|
}
|
|
if (http_st == ACME_HTTP_RES) {
|
|
enum acme_ret ret = acme_res_challenge(task, ctx, ctx->next_auth, 0, &errmsg);
|
|
|
|
if (ret == ACME_RET_RETRY) {
|
|
goto retry;
|
|
} else if (ret == ACME_RET_FAIL) {
|
|
goto end;
|
|
}
|
|
if ((ctx->next_auth = ctx->next_auth->next) == NULL) {
|
|
st = ACME_CHKCHALLENGE;
|
|
ctx->next_auth = ctx->auths;
|
|
/* let 5 seconds before checking the challenge */
|
|
if (ctx->retryafter == 0)
|
|
ctx->retryafter = 5;
|
|
}
|
|
/* call with next auth or do the challenge step */
|
|
goto nextreq;
|
|
}
|
|
break;
|
|
case ACME_CHKCHALLENGE:
|
|
if (http_st == ACME_HTTP_REQ) {
|
|
/* if challenge is already validated we skip this stage */
|
|
if (ctx->next_auth->validated) {
|
|
if ((ctx->next_auth = ctx->next_auth->next) == NULL)
|
|
st = ACME_FINALIZE;
|
|
|
|
goto nextreq;
|
|
}
|
|
|
|
if (acme_post_as_get(task, ctx, ctx->next_auth->chall, &errmsg) != 0)
|
|
goto retry;
|
|
}
|
|
if (http_st == ACME_HTTP_RES) {
|
|
enum acme_ret ret = acme_res_challenge(task, ctx, ctx->next_auth, 1, &errmsg);
|
|
if (ret == ACME_RET_RETRY) {
|
|
goto retry;
|
|
} else if (ret == ACME_RET_FAIL) {
|
|
goto abort;
|
|
}
|
|
if ((ctx->next_auth = ctx->next_auth->next) == NULL)
|
|
st = ACME_FINALIZE;
|
|
|
|
/* do it with the next auth or finalize */
|
|
goto nextreq;
|
|
}
|
|
break;
|
|
case ACME_FINALIZE:
|
|
if (http_st == ACME_HTTP_REQ) {
|
|
if (acme_req_finalize(task, ctx, &errmsg) != 0)
|
|
goto retry;
|
|
}
|
|
if (http_st == ACME_HTTP_RES) {
|
|
if (acme_res_finalize(task, ctx, &errmsg) != 0) {
|
|
goto retry;
|
|
}
|
|
/* let 5 seconds to the server to generate the cert */
|
|
if (ctx->retryafter == 0)
|
|
ctx->retryafter = 5;
|
|
st = ACME_CHKORDER;
|
|
goto nextreq;
|
|
}
|
|
break;
|
|
case ACME_CHKORDER:
|
|
if (http_st == ACME_HTTP_REQ) {
|
|
if (acme_post_as_get(task, ctx, ctx->order, &errmsg) != 0)
|
|
goto retry;
|
|
}
|
|
if (http_st == ACME_HTTP_RES) {
|
|
if (acme_res_chkorder(task, ctx, &errmsg) != 0) {
|
|
goto retry;
|
|
}
|
|
st = ACME_CERTIFICATE;
|
|
goto nextreq;
|
|
}
|
|
break;
|
|
case ACME_CERTIFICATE:
|
|
if (http_st == ACME_HTTP_REQ) {
|
|
if (acme_post_as_get(task, ctx, ctx->certificate, &errmsg) != 0)
|
|
goto retry;
|
|
}
|
|
if (http_st == ACME_HTTP_RES) {
|
|
if (acme_res_certificate(task, ctx, &errmsg) != 0) {
|
|
goto retry;
|
|
}
|
|
goto end;
|
|
}
|
|
break;
|
|
|
|
case ACME_END:
|
|
goto end;
|
|
break;
|
|
|
|
}
|
|
|
|
/* this is called after initializing a request */
|
|
ctx->http_state = http_st;
|
|
ctx->state = st;
|
|
task->expire = TICK_ETERNITY;
|
|
return task;
|
|
|
|
nextreq:
|
|
/* this is called when changing step in the state machine */
|
|
http_st = ACME_HTTP_REQ;
|
|
ctx->retries = ACME_RETRY; /* reinit the retries */
|
|
ctx->http_state = http_st;
|
|
ctx->state = st;
|
|
|
|
if (ctx->retryafter == 0)
|
|
goto re; /* optimize by not leaving the task for the next httpreq to init */
|
|
|
|
/* if we have a retryafter, wait before next request (usually finalize) */
|
|
task->expire = tick_add(now_ms, ctx->retryafter * 1000);
|
|
ctx->retryafter = 0;
|
|
|
|
return task;
|
|
|
|
retry:
|
|
ctx->http_state = ACME_HTTP_REQ;
|
|
ctx->state = st;
|
|
|
|
ctx->retries--;
|
|
if (ctx->retries > 0) {
|
|
int delay = 1;
|
|
int i;
|
|
|
|
if (ctx->retryafter > 0) {
|
|
/* Use the Retry-After value from the header */
|
|
delay = ctx->retryafter;
|
|
ctx->retryafter = 0;
|
|
} else {
|
|
/* else does an exponential backoff * 3 */
|
|
for (i = 0; i < ACME_RETRY - ctx->retries; i++)
|
|
delay *= 3;
|
|
}
|
|
send_log(NULL, LOG_NOTICE, "acme: %s: %s, retrying in %ds (%d/%d retries)...\n", ctx->store->path, errmsg ? errmsg : "", delay, ACME_RETRY - ctx->retries, ACME_RETRY);
|
|
task->expire = tick_add(now_ms, delay * 1000);
|
|
|
|
} else {
|
|
send_log(NULL, LOG_NOTICE,"acme: %s: %s Aborting. (%d/%d)\n", ctx->store->path, errmsg ? errmsg : "", ACME_RETRY-ctx->retries, ACME_RETRY);
|
|
goto end;
|
|
}
|
|
|
|
ha_free(&errmsg);
|
|
|
|
return task;
|
|
|
|
abort:
|
|
send_log(NULL, LOG_NOTICE,"acme: %s: %s Aborting.\n", ctx->store->path, errmsg ? errmsg : "");
|
|
ha_free(&errmsg);
|
|
|
|
end:
|
|
acme_del_acme_ctx_map(ctx);
|
|
HA_RWLOCK_WRLOCK(OTHER_LOCK, &acme_lock);
|
|
ebmb_delete(&ctx->node);
|
|
HA_RWLOCK_WRUNLOCK(OTHER_LOCK, &acme_lock);
|
|
acme_ctx_destroy(ctx);
|
|
task_destroy(task);
|
|
task = NULL;
|
|
|
|
return task;
|
|
|
|
wait:
|
|
/* wait for a task_wakeup */
|
|
ctx->http_state = ACME_HTTP_REQ;
|
|
ctx->state = st;
|
|
task->expire = TICK_ETERNITY;
|
|
return task;
|
|
}
|
|
|
|
/*
|
|
* Return when the next task is scheduled
|
|
* Check if the notAfter date will happen in (validity period / 12) or 7 days per default
|
|
*/
|
|
static time_t acme_schedule_date(struct ckch_store *store)
|
|
{
|
|
time_t diff = 0;
|
|
time_t notAfter = 0;
|
|
time_t notBefore = 0;
|
|
|
|
if (!global_ssl.acme_scheduler)
|
|
return 0;
|
|
|
|
/* compute the validity period of the leaf certificate */
|
|
if (!store->data || !store->data->cert)
|
|
return 0;
|
|
|
|
notAfter = x509_get_notafter_time_t(store->data->cert);
|
|
notBefore = x509_get_notbefore_time_t(store->data->cert);
|
|
|
|
if ((notAfter >= 0 && notBefore >= 0)
|
|
&& (notAfter > notBefore)) {
|
|
diff = (notAfter - notBefore) / 12; /* validity period / 12 */
|
|
} else {
|
|
diff = 7 * 24 * 60 * 60; /* default to 7 days */
|
|
}
|
|
if (notAfter > diff) /* avoid overflow */
|
|
return (notAfter - diff);
|
|
else
|
|
return 1; /* epoch+1 is long way expired */
|
|
}
|
|
|
|
/*
|
|
* Return 1 if the certificate must be regenerated
|
|
* Check if the notAfter date will append in (validity period / 12) or 7 days per default
|
|
*/
|
|
static int acme_will_expire(struct ckch_store *store)
|
|
{
|
|
time_t notAfter = 0;
|
|
|
|
/* compute the validity period of the leaf certificate */
|
|
if (!store->data || !store->data->cert)
|
|
return 0;
|
|
|
|
notAfter = acme_schedule_date(store);
|
|
|
|
if (notAfter <= date.tv_sec)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Does the scheduling of the ACME tasks
|
|
*/
|
|
struct task *acme_scheduler(struct task *task, void *context, unsigned int state)
|
|
{
|
|
struct ebmb_node *node = NULL;
|
|
struct ckch_store *store = NULL;
|
|
char *errmsg = NULL;
|
|
|
|
if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock))
|
|
return task;
|
|
|
|
node = ebmb_first(&ckchs_tree);
|
|
while (node) {
|
|
store = ebmb_entry(node, struct ckch_store, node);
|
|
|
|
if (store->conf.acme.id) {
|
|
|
|
if (acme_will_expire(store)) {
|
|
TRACE_USER("ACME Scheduling start", ACME_EV_SCHED);
|
|
if (acme_start_task(store, &errmsg) != 0) {
|
|
send_log(NULL, LOG_NOTICE,"acme: %s: %s Aborting.\n", store->path, errmsg ? errmsg : "");
|
|
ha_free(&errmsg);
|
|
}
|
|
}
|
|
}
|
|
node = ebmb_next(node);
|
|
}
|
|
end:
|
|
HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
|
|
/* call the task again in 12h */
|
|
/* XXX: need to be configured */
|
|
task->expire = tick_add(now_ms, 12 * 60 * 60 * 1000);
|
|
return task;
|
|
}
|
|
|
|
/*
|
|
* Generate a X509_REQ using a PKEY and a list of SAN finished by a NULL entry
|
|
*/
|
|
X509_REQ *acme_x509_req(EVP_PKEY *pkey, char **san, char **ips)
|
|
{
|
|
struct buffer *san_trash = NULL;
|
|
X509_REQ *x = NULL;
|
|
X509_NAME *nm = NULL;
|
|
STACK_OF(X509_EXTENSION) *exts = NULL;
|
|
X509_EXTENSION *ext_san = NULL;
|
|
char *str_san = NULL;
|
|
int i = 0;
|
|
|
|
if ((san_trash = alloc_trash_chunk()) == NULL)
|
|
goto error;
|
|
|
|
if ((x = X509_REQ_new()) == NULL)
|
|
goto error;
|
|
|
|
if (!X509_REQ_set_pubkey(x, pkey))
|
|
goto error;
|
|
|
|
if ((nm = X509_NAME_new()) == NULL)
|
|
goto error;
|
|
|
|
/* common name is the first domain, or the first IP if no domain */
|
|
if (!X509_NAME_add_entry_by_txt(nm, "CN", MBSTRING_ASC,
|
|
(unsigned char *)(san ? san[0] : ips[0]), -1, -1, 0))
|
|
goto error;
|
|
/* assign the CN to the REQ */
|
|
if (!X509_REQ_set_subject_name(x, nm))
|
|
goto error;
|
|
|
|
/* Add the SANs */
|
|
if ((exts = sk_X509_EXTENSION_new_null()) == NULL)
|
|
goto error;
|
|
|
|
for (i = 0; san && san[i]; i++) {
|
|
chunk_appendf(san_trash, "%sDNS:%s", san_trash->data ? "," : "", san[i]);
|
|
}
|
|
for (i = 0; ips && ips[i]; i++) {
|
|
chunk_appendf(san_trash, "%sIP:%s", san_trash->data ? "," : "", ips[i]);
|
|
}
|
|
if ((str_san = my_strndup(san_trash->area, san_trash->data)) == NULL)
|
|
goto error;
|
|
|
|
if ((ext_san = X509V3_EXT_conf_nid(NULL, NULL, NID_subject_alt_name, str_san)) == NULL)
|
|
goto error;
|
|
|
|
if (!sk_X509_EXTENSION_push(exts, ext_san))
|
|
goto error;
|
|
|
|
ext_san = NULL; /* handle double-free upon error */
|
|
|
|
if (!X509_REQ_add_extensions(x, exts))
|
|
goto error;
|
|
|
|
if (!X509_REQ_sign(x, pkey, EVP_sha256()))
|
|
goto error;
|
|
|
|
sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
|
|
X509_NAME_free(nm);
|
|
free(str_san);
|
|
free_trash_chunk(san_trash);
|
|
|
|
return x;
|
|
|
|
error:
|
|
X509_EXTENSION_free(ext_san);
|
|
sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
|
|
X509_REQ_free(x);
|
|
X509_NAME_free(nm);
|
|
free(str_san);
|
|
free_trash_chunk(san_trash);
|
|
return NULL;
|
|
|
|
}
|
|
|
|
/*
|
|
* Generate a temporary expired X509 or reuse the one generated.
|
|
* Use tmp_pkey to generate
|
|
*
|
|
* Increment the refcount when returning the existing one
|
|
*/
|
|
X509 *acme_gen_tmp_x509()
|
|
{
|
|
if (tmp_x509) {
|
|
X509_up_ref(tmp_x509);
|
|
return tmp_x509;
|
|
}
|
|
|
|
if (!tmp_pkey)
|
|
return NULL;
|
|
|
|
tmp_x509 = ssl_gen_x509(tmp_pkey);
|
|
|
|
return tmp_x509;
|
|
}
|
|
|
|
/*
|
|
* Generate a temporary RSA2048 pkey or reuse the one generated.
|
|
*
|
|
* Increment the refcount when returning the existing one
|
|
*
|
|
*/
|
|
EVP_PKEY *acme_gen_tmp_pkey()
|
|
{
|
|
if (tmp_pkey) {
|
|
EVP_PKEY_up_ref(tmp_pkey);
|
|
return tmp_pkey;
|
|
}
|
|
|
|
tmp_pkey = ssl_gen_EVP_PKEY(EVP_PKEY_RSA, 0, 2048, NULL);
|
|
|
|
return tmp_pkey;
|
|
}
|
|
|
|
|
|
/* start an ACME task */
|
|
static int acme_start_task(struct ckch_store *store, char **errmsg)
|
|
{
|
|
struct task *task = NULL;
|
|
struct acme_ctx *ctx = NULL;
|
|
struct acme_cfg *cfg;
|
|
struct ckch_store *newstore = NULL;
|
|
EVP_PKEY *pkey = NULL;
|
|
|
|
if (!store->conf.acme.domains && !store->conf.acme.ips) {
|
|
memprintf(errmsg, "No 'domains' or 'ips' were configured for certificate. ");
|
|
goto err;
|
|
}
|
|
|
|
cfg = get_acme_cfg(store->conf.acme.id);
|
|
if (!cfg) {
|
|
memprintf(errmsg, "No ACME configuration found for file '%s'.", store->path);
|
|
goto err;
|
|
}
|
|
|
|
newstore = ckchs_dup(store);
|
|
if (!newstore) {
|
|
memprintf(errmsg, "Out of memory.");
|
|
goto err;
|
|
}
|
|
|
|
task = task_new_anywhere();
|
|
if (!task)
|
|
goto err;
|
|
task->nice = 0;
|
|
task->process = acme_process;
|
|
|
|
/* XXX: following init part could be done in the task */
|
|
ctx = calloc(1, sizeof *ctx + strlen(newstore->path) + 1);
|
|
if (!ctx) {
|
|
memprintf(errmsg, "Out of memory.");
|
|
goto err;
|
|
}
|
|
|
|
memcpy(ctx->name, newstore->path, strlen(newstore->path) + 1);
|
|
|
|
HA_RWLOCK_WRLOCK(OTHER_LOCK, &acme_lock);
|
|
if (ebst_insert(&acme_tasks, &ctx->node) != &ctx->node) {
|
|
memprintf(errmsg, "%sTask already exists for '%s' certificate.\n", *errmsg ? *errmsg : "", newstore->path);
|
|
HA_RWLOCK_WRUNLOCK(OTHER_LOCK, &acme_lock);
|
|
goto err;
|
|
}
|
|
HA_RWLOCK_WRUNLOCK(OTHER_LOCK, &acme_lock);
|
|
|
|
/* set the number of remaining retries when facing an error */
|
|
ctx->retries = ACME_RETRY;
|
|
|
|
if (!cfg->reuse_key) {
|
|
if ((pkey = ssl_gen_EVP_PKEY(cfg->key.type, cfg->key.curves, cfg->key.bits, errmsg)) == NULL)
|
|
goto err;
|
|
|
|
EVP_PKEY_free(newstore->data->key);
|
|
newstore->data->key = pkey;
|
|
pkey = NULL;
|
|
}
|
|
|
|
ctx->req = acme_x509_req(newstore->data->key, store->conf.acme.domains, store->conf.acme.ips);
|
|
if (!ctx->req) {
|
|
memprintf(errmsg, "%sCan't generate a CSR.", *errmsg ? *errmsg : "");
|
|
goto err;
|
|
}
|
|
|
|
ctx->store = newstore;
|
|
ctx->cfg = cfg;
|
|
task->context = ctx;
|
|
ctx->task = task;
|
|
|
|
send_log(NULL, LOG_NOTICE, "acme: %s: Starting update of the certificate.\n", ctx->store->path);
|
|
|
|
TRACE_USER("ACME Task start", ACME_EV_NEW, ctx);
|
|
task_wakeup(task, TASK_WOKEN_INIT);
|
|
|
|
return 0;
|
|
|
|
err:
|
|
EVP_PKEY_free(pkey);
|
|
ckch_store_free(newstore);
|
|
if (ctx) {
|
|
HA_RWLOCK_WRLOCK(OTHER_LOCK, &acme_lock);
|
|
ebmb_delete(&ctx->node);
|
|
HA_RWLOCK_WRUNLOCK(OTHER_LOCK, &acme_lock);
|
|
acme_ctx_destroy(ctx);
|
|
}
|
|
if (task)
|
|
task_destroy(task);
|
|
memprintf(errmsg, "%sCan't start the ACME client.", *errmsg ? *errmsg : "");
|
|
return 1;
|
|
}
|
|
|
|
static int cli_acme_renew_parse(char **args, char *payload, struct appctx *appctx, void *private)
|
|
{
|
|
struct ckch_store *store = NULL;
|
|
char *errmsg = NULL;
|
|
|
|
if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
|
|
return 1;
|
|
|
|
if (!*args[2]) {
|
|
memprintf(&errmsg, ": not enough parameters\n");
|
|
goto err;
|
|
}
|
|
|
|
if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock))
|
|
return cli_err(appctx, "Can't update: operations on certificates are currently locked!\n");
|
|
|
|
if ((store = ckchs_lookup(args[2])) == NULL) {
|
|
memprintf(&errmsg, "Can't find the certificate '%s'.\n", args[2]);
|
|
goto err;
|
|
}
|
|
|
|
if (store->conf.acme.id == NULL) {
|
|
memprintf(&errmsg, "No ACME configuration defined for file '%s'.\n", args[2]);
|
|
goto err;
|
|
}
|
|
|
|
if (acme_start_task(store, &errmsg) != 0)
|
|
goto err;
|
|
|
|
HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
|
|
return 0;
|
|
err:
|
|
HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
|
|
return cli_dynerr(appctx, errmsg);
|
|
}
|
|
|
|
static int cli_acme_chall_ready_parse(char **args, char *payload, struct appctx *appctx, void *private)
|
|
{
|
|
char *msg = NULL;
|
|
const char *crt;
|
|
const char *dns;
|
|
struct acme_ctx *ctx = NULL;
|
|
struct acme_auth *auth = NULL;
|
|
int found = 0;
|
|
int remain = 0;
|
|
struct ebmb_node *node = NULL;
|
|
|
|
if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
|
|
return 1;
|
|
|
|
if (!*args[2] || !*args[3] || !*args[4]) {
|
|
memprintf(&msg, "Not enough parameters: \"acme challenge_ready <certfile> domain <domain>\"\n");
|
|
goto err;
|
|
}
|
|
|
|
crt = args[2];
|
|
dns = args[4];
|
|
|
|
HA_RWLOCK_WRLOCK(OTHER_LOCK, &acme_lock);
|
|
node = ebst_lookup(&acme_tasks, crt);
|
|
if (node) {
|
|
ctx = ebmb_entry(node, struct acme_ctx, node);
|
|
if (ctx->cfg->cond_ready & ACME_RDY_CLI)
|
|
auth = ctx->auths;
|
|
while (auth) {
|
|
if (strncmp(dns, auth->dns.ptr, auth->dns.len) == 0) {
|
|
if (!(auth->ready & ACME_RDY_CLI)) {
|
|
auth->ready |= ACME_RDY_CLI;
|
|
found++;
|
|
} else {
|
|
memprintf(&msg, "ACME challenge for crt \"%s\" and dns \"%s\" was already READY !\n", crt, dns);
|
|
}
|
|
}
|
|
if ((auth->ready & ACME_RDY_CLI) == 0)
|
|
remain++;
|
|
auth = auth->next;
|
|
}
|
|
}
|
|
HA_RWLOCK_WRUNLOCK(OTHER_LOCK, &acme_lock);
|
|
if (!found) {
|
|
if (!msg)
|
|
memprintf(&msg, "Couldn't find an ACME task using crt \"%s\" and dns \"%s\" to set as ready!\n", crt, dns);
|
|
goto err;
|
|
} else {
|
|
if (!remain) {
|
|
if (ctx)
|
|
task_wakeup(ctx->task, TASK_WOKEN_MSG);
|
|
return cli_dynmsg(appctx, LOG_INFO, memprintf(&msg, "%d '%s' challenge(s) ready! All challenges ready, starting challenges validation!", found, dns));
|
|
} else {
|
|
return cli_dynmsg(appctx, LOG_INFO, memprintf(&msg, "%d '%s' challenge(s) ready! Remaining challenges to deploy: %d", found, dns, remain));
|
|
}
|
|
}
|
|
|
|
err:
|
|
return cli_dynerr(appctx, msg);
|
|
}
|
|
|
|
static int cli_acme_status_io_handler(struct appctx *appctx)
|
|
{
|
|
struct ebmb_node *node = NULL;
|
|
struct ckch_store *store = NULL;
|
|
|
|
if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock))
|
|
return 1;
|
|
|
|
chunk_reset(&trash);
|
|
|
|
chunk_appendf(&trash, "# certificate\tsection\tstate\texpiration date (UTC)\texpires in\tscheduled date (UTC)\tscheduled in\n");
|
|
if (applet_putchk(appctx, &trash) == -1)
|
|
return 1;
|
|
|
|
|
|
if (applet_putchk(appctx, &trash) == -1)
|
|
return 1;
|
|
|
|
/* TODO: handle backref list when list of task > buffer size */
|
|
node = ebmb_first(&ckchs_tree);
|
|
while (node) {
|
|
store = ebmb_entry(node, struct ckch_store, node);
|
|
|
|
if (store->conf.acme.id) {
|
|
char str[50] = {};
|
|
char *state;
|
|
time_t notAfter = 0;
|
|
time_t sched = 0;
|
|
ullong remain = 0;
|
|
int running = 0;
|
|
|
|
HA_RWLOCK_RDLOCK(OTHER_LOCK, &acme_lock);
|
|
running = !!ebst_lookup(&acme_tasks, store->path);
|
|
HA_RWLOCK_RDUNLOCK(OTHER_LOCK, &acme_lock);
|
|
|
|
if (global_ssl.acme_scheduler)
|
|
state = "Scheduled";
|
|
else
|
|
state = "Stopped";
|
|
|
|
if (running)
|
|
state = "Running";
|
|
|
|
chunk_appendf(&trash, "%s\t%s\t%s\t", store->path, store->conf.acme.id, state);
|
|
|
|
notAfter = x509_get_notafter_time_t(store->data->cert);
|
|
/* Expiration time */
|
|
if (notAfter > date.tv_sec)
|
|
remain = notAfter - date.tv_sec;
|
|
strftime(str, sizeof(str), "%Y-%m-%dT%H:%M:%SZ", gmtime(¬After));
|
|
chunk_appendf(&trash, "%s\t", str);
|
|
chunk_appendf(&trash, "%llud %lluh%02llum%02llus\t", remain / 86400, (remain % 86400) / 3600, (remain % 3600) / 60, (remain % 60));
|
|
|
|
/* Scheduled time */
|
|
remain = 0;
|
|
if (!running) /* if running no schedule date yet */
|
|
sched = acme_schedule_date(store);
|
|
if (sched > date.tv_sec)
|
|
remain = sched - date.tv_sec;
|
|
strftime(str, sizeof(str), "%Y-%m-%dT%H:%M:%SZ", gmtime(&sched));
|
|
chunk_appendf(&trash, "%s\t", sched ? str : "-");
|
|
if (sched)
|
|
chunk_appendf(&trash, "%llud %lluh%02llum%02llus\n", remain / 86400, (remain % 86400) / 3600, (remain % 3600) / 60, (remain % 60));
|
|
else
|
|
chunk_appendf(&trash, "%s\n", "-");
|
|
|
|
if (applet_putchk(appctx, &trash) == -1)
|
|
return 1;
|
|
}
|
|
node = ebmb_next(node);
|
|
}
|
|
end:
|
|
HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
|
|
return 1;
|
|
}
|
|
|
|
static int cli_acme_parse_status(char **args, char *payload, struct appctx *appctx, void *private)
|
|
{
|
|
|
|
if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
static struct cli_kw_list cli_kws = {{ },{
|
|
{ { "acme", "renew", NULL }, "acme renew <certfile> : renew a certificate using the ACME protocol", cli_acme_renew_parse, NULL, NULL, NULL, 0 },
|
|
{ { "acme", "status", NULL }, "acme status : show status of certificates configured with ACME", cli_acme_parse_status, cli_acme_status_io_handler, NULL, NULL, 0 },
|
|
{ { "acme", "challenge_ready", NULL }, "acme challenge_ready <certfile> domain <domain> : notify HAProxy that the ACME challenge is ready", cli_acme_chall_ready_parse, NULL, NULL, NULL, 0 },
|
|
{ { NULL }, NULL, NULL, NULL }
|
|
}};
|
|
|
|
INITCALL1(STG_REGISTER, cli_register_kw, &cli_kws);
|
|
|
|
static void __acme_init(void)
|
|
{
|
|
hap_register_feature("ACME");
|
|
HA_RWLOCK_INIT(&acme_lock);
|
|
}
|
|
INITCALL0(STG_REGISTER, __acme_init);
|
|
|
|
#endif /* ! HAVE_ACME */
|
|
|
|
/*
|
|
* Local variables:
|
|
* c-indent-level: 8
|
|
* c-basic-offset: 8
|
|
* End:
|
|
*/
|