haproxy/src/acme.c
William Lallemand 45fba1db27 BUG/MINOR: acme: avoid overflow when diff > notAfter
Avoid an overflow or a negative value if notAfter < diff.

This is unlikely to provoke any problem.

Fixes issue #3138.

Must be backported to 3.2.
2025-10-07 10:54:58 +02:00

2867 lines
76 KiB
C

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Implements the ACMEv2 RFC 8555 protocol
*/
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <import/ebsttree.h>
#include <import/mjson.h>
#include <import/mt_list.h>
#include <haproxy/acme-t.h>
#include <haproxy/cli.h>
#include <haproxy/cfgparse.h>
#include <haproxy/errors.h>
#include <haproxy/jws.h>
#include <haproxy/base64.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/sink.h>
#include <haproxy/ssl_ckch.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 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_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 mt_list acme_tasks = MT_LIST_HEAD_INIT(acme_tasks);
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 EVP_PKEY *acme_EVP_PKEY_gen(int keytype, int curves, int bits, char **errmsg);
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 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 */
/* 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;
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_data *d, 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;
}
if (strcmp(args[0], "acme") == 0) {
struct acme_cfg *tmp_acme = acme_cfgs;
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;
}
if (httpclient_acme_px == NULL) {
if (httpclient_acme_init() & ERR_FATAL) {
err_code |= ERR_ALERT | ERR_FATAL;
ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
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], tmp_acme->filename, tmp_acme->linenum);
goto out;
}
cur_acme->filename = (char *)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;
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;
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(2, file, linenum, args, &err_code))
goto out;
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], "challenge") == 0) {
if ((!*args[1]) || (strcasecmp("http-01", args[1]) != 0 && (strcasecmp("dns-01", args[1]) != 0))) {
ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires a challenge type: http-01 or dns-01\n", file, linenum, args[0], cursection);
err_code |= ERR_ALERT | ERR_FATAL;
goto out;
}
if (alertif_too_many_args(2, file, linenum, args, &err_code))
goto out;
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;
}
} 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;
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], "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 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 (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(errmsg, "'%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 ? errmsg : NULL, path);
if (errmsg && *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 = acme_EVP_PKEY_gen(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: out of memory.\n");
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;
}
/* insert into the ckchs tree */
ebst_insert(&ckchs_tree, &store->node);
out:
EVP_PKEY_free(key);
BIO_free_all(bio);
ha_free(&errmsg);
return err_code;
}
/* 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);
}
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->name);
ha_free(&acme_cfgs->directory);
ha_free(&acme_cfgs->account.contact);
ha_free(&acme_cfgs->account.file);
ha_free(&acme_cfgs->account.thumbprint);
ha_free(&acme_cfgs->vars);
ha_free(&acme_cfgs->provider);
free(acme_cfgs);
acme_cfgs = next;
}
}
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, "reuse-key", 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->token);
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;
}
/*
* 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; 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 = atol(hdr->v.ptr);
}
}
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;
/* 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 = atol(hdr->v.ptr);
}
}
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;
};
out:
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);
OPENSSL_free(data);
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;
error:
memprintf(errmsg, "couldn't request the finalize URL");
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; 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 = atol(hdr->v.ptr);
}
}
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;
}
out:
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;
error:
memprintf(errmsg, "couldn't generate the Challenge request");
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; 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 = atol(hdr->v.ptr);
}
}
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;
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;
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; 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 = atol(hdr->v.ptr);
}
}
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;
auth->dns = istdup(ist2(t2->area, t2->data));
/* 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;
}
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;
}
/* compute a response for the TXT entry */
if (strcasecmp(ctx->cfg->challenge, "dns-01") == 0) {
struct sink *dpapi;
struct ist line[13];
int nmsg = 0;
if (acme_txt_record(ist(ctx->cfg->account.thumbprint), auth->token, &trash) == 0) {
memprintf(errmsg, "couldn't compute the dns-01 challenge");
goto error;
}
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\" command over the CLI\n",
ctx->store->path, (int)auth->dns.len, auth->dns.ptr, (int)trash.data, trash.area);
/* 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");
}
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);
}
/* 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;
}
/* 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;
char **san = ctx->store->conf.acme.domains;
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)
goto error;
for (; san && *san; san++) {
// fprintf(stderr, "%s:%d %s\n", __FUNCTION__, __LINE__, *san);
chunk_appendf(req_in, "%s{ \"type\": \"dns\", \"value\": \"%s\" }", (*san == *ctx->store->conf.acme.domains) ? "" : ",", *san);
}
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;
error:
memprintf(errmsg, "couldn't generate the newOrder request");
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; 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 = atol(hdr->v.ptr);
}
/* 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 (!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)) {
memprintf(errmsg, "out of memory");
goto error;
}
/* if the challenge is not dns-01, consider that the challenge
* is ready because computed by HAProxy */
if (strcasecmp(ctx->cfg->challenge, "dns-01") != 0)
auth->ready = 1;
auth->next = ctx->auths;
ctx->auths = auth;
ctx->next_auth = auth;
}
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;
}
out:
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;
const struct http_hdr hdrs[] = {
{ IST("Content-Type"), IST("application/jose+json") },
{ IST_NULL, IST_NULL }
};
char *accountreq = "{\n"
" \"termsOfServiceAgreed\": true,\n"
" \"onlyReturnExisting\": true\n"
"}\n";
char *newaccountreq = "{\n"
" \"termsOfServiceAgreed\": true,\n"
" \"contact\": [\n"
" \"mailto:%s\"\n"
" ]\n"
"}\n";
int ret = 1;
if ((req_in = alloc_trash_chunk()) == NULL)
goto error;
if ((req_out = alloc_trash_chunk()) == NULL)
goto error;
if (newaccount)
chunk_printf(req_in, newaccountreq, ctx->cfg->account.contact);
else
chunk_printf(req_in, "%s", accountreq);
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;
error:
memprintf(errmsg, "couldn't generate the newAccount request");
free_trash_chunk(req_in);
free_trash_chunk(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; 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 = atol(hdr->v.ptr);
}
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; 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;
struct mt_list tmp = MT_LIST_LOCK_FULL(&ctx->el);
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 = 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) {
st = ACME_CHALLENGE;
ctx->next_auth = ctx->auths;
}
/* call with next auth or do the challenge step */
goto nextreq;
}
break;
case ACME_CHALLENGE:
if (http_st == ACME_HTTP_REQ) {
/* if the challenge is not ready, wait to be wakeup */
if (!ctx->next_auth->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 (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 */
MT_LIST_UNLOCK_FULL(&ctx->el, tmp);
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;
MT_LIST_UNLOCK_FULL(&ctx->el, tmp);
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);
MT_LIST_UNLOCK_FULL(&ctx->el, tmp);
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);
/* unlink ctx from the mtlist then destroy */
mt_list_unlock_link(tmp);
mt_list_unlock_self(&ctx->el);
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;
MT_LIST_UNLOCK_FULL(&ctx->el, tmp);
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)
{
struct buffer *san_trash = NULL;
X509_REQ *x = NULL;
X509_NAME *nm;
STACK_OF(X509_EXTENSION) *exts = NULL;
X509_EXTENSION *ext_san;
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 SAN in the list */
if (!X509_NAME_add_entry_by_txt(nm, "CN", MBSTRING_ASC,
(unsigned char *)san[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[i]; i++) {
chunk_appendf(san_trash, "%sDNS:%s", i ? "," : "", san[i]);
}
str_san = my_strndup(san_trash->area, san_trash->data);
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;
if (!X509_REQ_add_extensions(x, exts))
goto error;
sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
if (!X509_REQ_sign(x, pkey, EVP_sha256()))
goto error;
free_trash_chunk(san_trash);
return x;
error:
free_trash_chunk(san_trash);
return NULL;
}
/* Return a new Generated private key of type <keytype> with <bits> and <curves> */
static EVP_PKEY *acme_EVP_PKEY_gen(int keytype, int curves, int bits, char **errmsg)
{
EVP_PKEY_CTX *pkey_ctx = NULL;
EVP_PKEY *pkey = NULL;
if ((pkey_ctx = EVP_PKEY_CTX_new_id(keytype, NULL)) == NULL) {
memprintf(errmsg, "%sCan't generate a private key.\n", *errmsg ? *errmsg : "");
goto err;
}
if (EVP_PKEY_keygen_init(pkey_ctx) <= 0) {
memprintf(errmsg, "%sCan't generate a private key.\n", *errmsg ? *errmsg : "");
goto err;
}
if (keytype == EVP_PKEY_EC) {
if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pkey_ctx, curves) <= 0) {
memprintf(errmsg, "%sCan't set the curves on the new private key.\n", *errmsg ? *errmsg : "");
goto err;
}
} else if (keytype == EVP_PKEY_RSA) {
if (EVP_PKEY_CTX_set_rsa_keygen_bits(pkey_ctx, bits) <= 0) {
memprintf(errmsg, "%sCan't set the bits on the new private key.\n", *errmsg ? *errmsg : "");
goto err;
}
}
if (EVP_PKEY_keygen(pkey_ctx, &pkey) <= 0) {
memprintf(errmsg, "%sCan't generate a private key.\n", *errmsg ? *errmsg : "");
goto err;
}
err:
EVP_PKEY_CTX_free(pkey_ctx);
return pkey;
}
/* start an ACME task */
static int acme_start_task(struct ckch_store *store, char **errmsg)
{
struct task *task;
struct acme_ctx *ctx = NULL;
struct acme_cfg *cfg;
struct ckch_store *newstore = NULL;
EVP_PKEY *pkey = NULL;
if (store->acme_task != NULL) {
memprintf(errmsg, "An ACME task is already running for certificate '%s'.", store->path);
goto err;
}
if (!store->conf.acme.domains) {
memprintf(errmsg, "No 'domains' 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;
/* register the task in the store so we don't
* have 2 tasks at the same time
*/
store->acme_task = task;
/* XXX: following init part could be done in the task */
ctx = calloc(1, sizeof *ctx);
if (!ctx) {
memprintf(errmsg, "Out of memory.");
goto err;
}
/* set the number of remaining retries when facing an error */
ctx->retries = ACME_RETRY;
if (!cfg->reuse_key) {
if ((pkey = acme_EVP_PKEY_gen(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);
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;
MT_LIST_INIT(&ctx->el);
MT_LIST_APPEND(&acme_tasks, &ctx->el);
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);
acme_ctx_destroy(ctx);
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 (!*args[1]) {
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 *errmsg = NULL;
const char *crt;
const char *dns;
struct mt_list back;
struct acme_ctx *ctx;
struct acme_auth *auth;
int found = 0;
if (!*args[2] && !*args[3] && !*args[4]) {
memprintf(&errmsg, ": not enough parameters\n");
goto err;
}
crt = args[2];
dns = args[4];
MT_LIST_FOR_EACH_ENTRY_LOCKED(ctx, &acme_tasks, el, back) {
if (strcmp(ctx->store->path, crt) != 0)
continue;
auth = ctx->auths;
while (auth) {
if (strncmp(dns, auth->dns.ptr, auth->dns.len) == 0) {
if (!auth->ready) {
auth->ready = 1;
task_wakeup(ctx->task, TASK_WOKEN_MSG);
found = 1;
} else {
memprintf(&errmsg, "ACME challenge for crt \"%s\" and dns \"%s\" was already READY !\n", crt, dns);
}
break;
}
auth = auth->next;
}
}
if (!found) {
memprintf(&errmsg, "Couldn't find the ACME task using crt \"%s\" and dns \"%s\" !\n", crt, dns);
goto err;
}
return cli_msg(appctx, LOG_INFO, "Challenge Ready!");
err:
return cli_dynerr(appctx, errmsg);
}
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 = !!store->acme_task;
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(&notAfter));
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_ps(char **args, char *payload, struct appctx *appctx, void *private)
{
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_ps, 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");
}
INITCALL0(STG_REGISTER, __acme_init);
#endif /* ! HAVE_ACME */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*/