From: William Lallemand Date: Wed, 2 Apr 2025 08:54:19 +0000 (+0200) Subject: MINOR: acme: add the acme section in the configuration parser X-Git-Tag: v3.2-dev11~102 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=077e2ce84c069e6083fb32bbb519c9be2002f395;p=thirdparty%2Fhaproxy.git MINOR: acme: add the acme section in the configuration parser Add a configuration parser for the new acme section, the section is configured this way: acme letsencrypt uri https://acme-staging-v02.api.letsencrypt.org/directory account account.key contact foobar@example.com challenge HTTP-01 When unspecified, the challenge defaults to HTTP-01, and the account key to ".account.key". Section are stored in a linked list containing acme_cfg structures, the configuration parsing is mostly resolved in the postsection parser cfg_postsection_acme() which is called after the parsing of an acme section. --- diff --git a/Makefile b/Makefile index 72a696b2a..4b87a6a09 100644 --- a/Makefile +++ b/Makefile @@ -631,7 +631,7 @@ ifneq ($(USE_OPENSSL:0=),) USE_SSL := $(if $(USE_SSL:0=),$(USE_SSL:0=),implicit) OPTIONS_OBJS += src/ssl_sock.o src/ssl_ckch.o src/ssl_ocsp.o src/ssl_crtlist.o \ src/ssl_sample.o src/cfgparse-ssl.o src/ssl_gencert.o \ - src/ssl_utils.o src/jwt.o src/ssl_clienthello.o src/jws.o + src/ssl_utils.o src/jwt.o src/ssl_clienthello.o src/jws.o src/acme.o endif ifneq ($(USE_ENGINE:0=),) diff --git a/include/haproxy/acme-t.h b/include/haproxy/acme-t.h new file mode 100644 index 000000000..127660e7a --- /dev/null +++ b/include/haproxy/acme-t.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#ifndef _ACME_T_H_ +#define _ACME_T_H_ + +#include + +/* acme section configuration */ +struct acme_cfg { + char *filename; /* config filename */ + int linenum; /* config linenum */ + char *name; /* section name */ + char *uri; /* directory URL */ + struct { + char *contact; /* email associated to account */ + char *file; /* account key filename */ + EVP_PKEY *pkey; /* account PKEY */ + char *thumbprint; /* account PKEY JWS thumbprint */ + } account; + char *challenge; /* HTTP-01, DNS-01, etc */ + struct acme_cfg *next; +}; + +#endif diff --git a/include/haproxy/cfgparse.h b/include/haproxy/cfgparse.h index 760d54e01..a20a30e80 100644 --- a/include/haproxy/cfgparse.h +++ b/include/haproxy/cfgparse.h @@ -39,6 +39,7 @@ struct acl_cond; #define CFG_CRTLIST 5 #define CFG_CRTSTORE 6 #define CFG_TRACES 7 +#define CFG_ACME 8 /* various keyword modifiers */ enum kw_mod { diff --git a/src/acme.c b/src/acme.c new file mode 100644 index 000000000..4b352a0c7 --- /dev/null +++ b/src/acme.c @@ -0,0 +1,351 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/* + * Implements the ACMEv2 RFC 8555 protocol + */ + +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +static struct acme_cfg *acme_cfgs = NULL; +static struct acme_cfg *cur_acme = NULL; + +/* 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 */ + + ret->next = acme_cfgs; + acme_cfgs = ret; + +out: + return ret; +} + +/* 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 (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; + } + + 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 unitialized 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], "uri") == 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->uri = strdup(args[1]); + if (!cur_acme->uri) { + 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") == 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]) || (strcmp("HTTP-01", args[1]) != 0 && (strcmp("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 (*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; +} + +/* Initialize stuff once the section is parsed */ +static int cfg_postsection_acme() +{ + struct acme_cfg *cur_acme = acme_cfgs; + struct ckch_store *store; + int err_code = 0; + char *errmsg = NULL; + char *path; + struct stat st; + + /* TODO: generate a key at startup and dumps on the filesystem + * TODO: use the standard ckch loading for the account key (need a store with only a key) + */ + + /* 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; + } + } + + path = cur_acme->account.file; + + 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_alert("%s '%s' is not present and can't be generated, please provide an account file.\n", errmsg, path); + err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT; + goto out; + } + + + 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; + + trash.data = jws_thumbprint(cur_acme->account.pkey, trash.area, trash.size); + + cur_acme->account.thumbprint = 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: + ha_free(&errmsg); + return err_code; +} + +void deinit_acme() +{ + struct acme_cfg *next = NULL; + + while (acme_cfgs) { + + next = acme_cfgs->next; + ha_free(&acme_cfgs->name); + ha_free(&acme_cfgs->uri); + 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, "uri", cfg_parse_acme_kws }, + { CFG_ACME, "contact", cfg_parse_acme_kws }, + { CFG_ACME, "account", cfg_parse_acme_kws }, + { CFG_ACME, "challenge", cfg_parse_acme_kws }, + { 0, NULL, NULL }, +}}; + +INITCALL1(STG_REGISTER, cfg_register_keywords, &cfg_kws_acme); + +REGISTER_CONFIG_SECTION("acme", cfg_parse_acme, cfg_postsection_acme); + + +/* + * Local variables: + * c-indent-level: 8 + * c-basic-offset: 8 + * End: + */