mirror of
https://git.haproxy.org/git/haproxy.git/
synced 2025-08-06 15:17:01 +02:00
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 "<section_name>.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.
This commit is contained in:
parent
20718f40b6
commit
077e2ce84c
2
Makefile
2
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=),)
|
||||
|
23
include/haproxy/acme-t.h
Normal file
23
include/haproxy/acme-t.h
Normal file
@ -0,0 +1,23 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#ifndef _ACME_T_H_
|
||||
#define _ACME_T_H_
|
||||
|
||||
#include <haproxy/openssl-compat.h>
|
||||
|
||||
/* 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
|
@ -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 {
|
||||
|
351
src/acme.c
Normal file
351
src/acme.c
Normal file
@ -0,0 +1,351 @@
|
||||
/* 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 <haproxy/acme-t.h>
|
||||
|
||||
#include <haproxy/cfgparse.h>
|
||||
#include <haproxy/errors.h>
|
||||
#include <haproxy/jws.h>
|
||||
#include <haproxy/ssl_ckch.h>
|
||||
#include <haproxy/ssl_sock.h>
|
||||
#include <haproxy/tools.h>
|
||||
|
||||
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:
|
||||
*/
|
Loading…
Reference in New Issue
Block a user