mirror of
https://git.haproxy.org/git/haproxy.git/
synced 2025-09-21 13:51:26 +02:00
Both the RFC and the IANA registry refers to challenge names in lowercase. If we need to implement more challenges, it's better to use the correct naming. In order to keep the compatibility with the previous configurations, the parsing does a strcasecmp() instead of a strcmp(). Also rename every occurence in the code and doc in lowercase. This was discussed in issue #1864
2763 lines
73 KiB
C
2763 lines
73 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 (*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;
|
|
}
|
|
|
|
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 acme_cfg *cur_acme = acme_cfgs;
|
|
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");
|
|
}
|
|
task->nice = 0;
|
|
task->process = acme_scheduler;
|
|
|
|
task_wakeup(task, TASK_WOKEN_INIT);
|
|
}
|
|
|
|
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);
|
|
|
|
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_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);
|
|
|
|
MT_LIST_DELETE(&ctx->el);
|
|
|
|
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);
|
|
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[7];
|
|
|
|
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[0] = ist("acme deploy ");
|
|
line[1] = ist(ctx->store->path);
|
|
line[2] = ist(" thumbprint ");
|
|
line[3] = ist(ctx->cfg->account.thumbprint);
|
|
line[4] = ist("\n");
|
|
line[5] = ist2( hc->res.buf.area, hc->res.buf.data); /* dump the HTTP response */
|
|
line[6] = ist("\n\0");
|
|
|
|
dpapi = sink_find("dpapi");
|
|
if (dpapi)
|
|
sink_write(dpapi, LOG_HEADER_NONE, 0, line, 7);
|
|
}
|
|
|
|
/* 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:
|
|
MT_LIST_UNLOCK_FULL(&ctx->el, tmp);
|
|
acme_del_acme_ctx_map(ctx);
|
|
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 1 if the certificate must be regenerated
|
|
* Check if the notAfter date will append in (validity period / 12) or 7 days per default
|
|
*/
|
|
int acme_will_expire(struct ckch_store *store)
|
|
{
|
|
int diff = 0;
|
|
time_t notAfter = 0;
|
|
time_t notBefore = 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) {
|
|
diff = (notAfter - notBefore) / 12; /* validity period / 12 */
|
|
} else {
|
|
diff = 7 * 24 * 60 * 60; /* default to 7 days */
|
|
}
|
|
|
|
if (date.tv_sec + diff > notAfter)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Return when the next task is scheduled
|
|
* Check if the notAfter date will happen in (validity period / 12) or 7 days per default
|
|
*/
|
|
time_t acme_schedule_date(struct ckch_store *store)
|
|
{
|
|
int 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) {
|
|
diff = (notAfter - notBefore) / 12; /* validity period / 12 */
|
|
} else {
|
|
diff = 7 * 24 * 60 * 60; /* default to 7 days */
|
|
}
|
|
|
|
return (notAfter - diff);
|
|
}
|
|
|
|
/* 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 ((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(¬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_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> : show status of certificates configured with ACME", 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:
|
|
*/
|