Patch introduces ACME EAB support.

Configuring EAB requires two parts: Key ID and MAC Key.
Key ID is an ASCII string that specifies the name of the record CA
should look up. MAC Key is a base64url encoded key that is used
for the sake of JWS signing, using HS256 or other algorithms.
They are the credentials so must be stored securely.

A thing about EAB is that it is required only during account creation
so it is unexpectedly complex to think about.
Some CAs provide EAB credential pair that is reused between
multiple account order requests, for example ZeroSSL, but others like
Google Trusted Services require an unique EAB credential for each new
account creation request.

There are a lot of ways config could be implemented, I decided to make
so that Key ID and MAC Key are stored in separate files on disk,
that decision was made because of the security concerns.
File based approach in particular works well with systemd credentials,
works well with systems that have config world readable, or immutable,
and is compatible with existing setups that specify credentials in a
file.

EAB is configured through options like this in an acme section:

eab-mac-alg HS512
eab-mac-key pebble.eab.mac-key
eab-key-id pebble.eab.key-id

I decided to not error out on empty files, but issue a log msg instead,
so that credentials can be removed without changing the haproxy config.

Used read_line_to_trash function from tools.c for reading files,
that is something that could be replaced by a dedicated function too.

No backport needed
---
 doc/configuration.txt    |  13 ++++
 include/haproxy/acme-t.h |   7 ++
 src/acme.c               | 160 ++++++++++++++++++++++++++++++++++++++-
 3 files changed, 177 insertions(+), 3 deletions(-)

diff --git a/doc/configuration.txt b/doc/configuration.txt
index 3316db7c9..0cf949ef3 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -32660,6 +32660,19 @@ Example:
       curves P-384
       map virt@acme
 
+eab-mac-key <filename>
+eab-key-id <filename>
+  Configure the path to the EAB MAC key and EAB key id credential pair. You
+  should get credentials from you CA and place them at the specified path
+  before launching HAProxy, they will be used during the account creation.
+
+  EAB key id file should be a plain ASCII string that CA provides as an id.
+  EAB MAC key file should be a base64url encoded MAC key that CA provides.
+
+  EAB credentials are used only during the initial ACME account creation, and
+  can be removed afterwards, either from the config or by emptying the files.
+  If file is empty HAProxy will ignore it. Whitespace in is not ignored,
+  except the trailing newline.
 
 12.9. Healthchecks
 ------------------
diff --git a/include/haproxy/acme-t.h b/include/haproxy/acme-t.h
index 24df7c44a..87f3bfffa 100644
--- a/include/haproxy/acme-t.h
+++ b/include/haproxy/acme-t.h
@@ -4,6 +4,7 @@
 
 #include <haproxy/acme_resolvers-t.h>
 #include <haproxy/istbuf.h>
+#include <haproxy/buf-t.h>
 #include <haproxy/openssl-compat.h>
 
 #if defined(HAVE_ACME)
@@ -40,6 +41,12 @@ struct acme_cfg {
                int bits;                   /* bits for RSA */
                int curves;                 /* NID of curves */
        } key;
+       struct {
+               char *kid_file;        /* EAB key id filename */
+               char *mac_key_file;    /* base64url encoded EAB hmac key 
filename */
+               char *kid;             /* EAB key id */
+               struct buffer mac_key; /* raw EAB hmac key */
+       } eab;
        char *challenge;            /* HTTP-01, DNS-01, etc */
        char *profile;              /* ACME profile */
        char *vars;                 /* variables put in the dpapi sink */
diff --git a/src/acme.c b/src/acme.c
index 46d5ca90a..637d472b9 100644
--- a/src/acme.c
+++ b/src/acme.c
@@ -418,6 +418,38 @@ static int cfg_parse_acme_kws(char **args, int 
section_type, struct proxy *curpx
                        ha_alert("parsing [%s:%d]: out of memory.\n", file, 
linenum);
                        goto out;
                }
+       } else if (strcmp(args[0], "eab-key-id") == 0) {
+               if (!*args[1]) {
+                       ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section 
requires an argument\n", file, linenum, args[0], cursection);
+                       err_code |= ERR_ALERT | ERR_FATAL;
+                       goto out;
+               }
+               if (alertif_too_many_args(1, file, linenum, args, &err_code))
+                       goto out;
+
+               ha_free(&cur_acme->eab.kid_file);
+               cur_acme->eab.kid_file = strdup(args[1]);
+               if (!cur_acme->eab.kid_file) {
+                       err_code |= ERR_ALERT | ERR_FATAL;
+                       ha_alert("parsing [%s:%d]: out of memory.\n", file, 
linenum);
+                       goto out;
+               }
+       } else if (strcmp(args[0], "eab-mac-key") == 0) {
+               if (!*args[1]) {
+                       ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section 
requires an argument\n", file, linenum, args[0], cursection);
+                       err_code |= ERR_ALERT | ERR_FATAL;
+                       goto out;
+               }
+               if (alertif_too_many_args(1, file, linenum, args, &err_code))
+                       goto out;
+
+               ha_free(&cur_acme->eab.mac_key_file);
+               cur_acme->eab.mac_key_file = strdup(args[1]);
+               if (!cur_acme->eab.mac_key_file) {
+                       err_code |= ERR_ALERT | ERR_FATAL;
+                       ha_alert("parsing [%s:%d]: out of memory.\n", file, 
linenum);
+                       goto out;
+               }
        } else if (strcmp(args[0], "challenge") == 0) {
                if ((!*args[1]) ||
                    ((strcasecmp("http-01", args[1]) != 0) &&
@@ -814,6 +846,75 @@ static int cfg_postsection_acme()
                }
        }
 
+       if (cur_acme->eab.kid_file != NULL && cur_acme->eab.mac_key_file != 
NULL) {
+               int rv = 0;
+               rv = read_line_to_trash("%s", cur_acme->eab.kid_file);
+               if (rv >= 1) {
+                       /* if read at least one character successfully */
+                       const char *p;
+
+                       cur_acme->eab.kid = my_strndup(trash.area, trash.data);
+                       if (!cur_acme->eab.kid) {
+                               ha_alert("acme: out of memory.\n");
+                               err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
+                               goto out;
+                       }
+
+                       /* technically ACME RFC allows any ASCII string here,
+                        * but in practice CAs usually provide key id as a 
base64url encoded secret or an UUID
+                        * this warning may need to be adjusted in the future */
+                       for (p = cur_acme->eab.kid; *p; p++) {
+                               if (!isalnum((uchar)*p) && *p != '-' && *p != 
'_') {
+                                       ha_warning("acme: section '%s': EAB key 
id contains strange character '%c'.\n",  cur_acme->name, *p);
+                                       break; /* no need to print this warning 
many times */
+                               }
+                       }
+               } else if (rv == 0) {
+                       /* empty files are allowed, but issue a log message */
+                       ha_notice("acme: section '%s': EAB key id from '%s' is 
empty.\n", cur_acme->name, cur_acme->eab.kid_file);
+               } else {
+                       ha_alert("acme: section '%s': couldn't load EAB key id 
from '%s', code %d.\n", cur_acme->name, cur_acme->eab.mac_key_file, rv);
+                       err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
+                       goto out;
+               }
+
+               rv = read_line_to_trash("%s", cur_acme->eab.mac_key_file);
+               if (rv >= 1) {
+                       struct buffer *dec_mac = get_trash_chunk();
+                       int bytes = 0;
+
+                       bytes = base64dec(trash.area, trash.data, 
dec_mac->area, dec_mac->size);
+                       if (bytes < 0) {
+                               ha_alert("acme: section '%s': failed to 
base64url decode EAB MAC key.\n", cur_acme->name);
+                               err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
+                               goto out;
+                       }
+                       dec_mac->data = bytes;
+
+                       if (bytes < 32) {
+                               ha_alert("acme: section '%s': EAB MAC key from 
'%s' is only %d bytes long, but at least 32 bytes is required for the specified 
MAC type.\n",
+                                    cur_acme->name, cur_acme->eab.kid_file, 
bytes);
+                               err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
+                               goto out;
+                       }
+
+                       if (chunk_dup(&cur_acme->eab.mac_key, dec_mac) == NULL) 
{
+                               ha_alert("acme: out of memory.\n");
+                               err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
+                               goto out;
+                       }
+               } else if (rv == 0) {
+                       ha_notice("acme: section '%s': EAB MAC key from '%s' is 
empty.\n", cur_acme->name, cur_acme->eab.mac_key_file);
+               } else {
+                       ha_alert("acme: section '%s': couldn't load EAB MAC key 
from '%s', code %d.\n", cur_acme->name, cur_acme->eab.mac_key_file, rv);
+                       err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
+                       goto out;
+               }
+       } else if ((cur_acme->eab.kid_file == NULL) != 
(cur_acme->eab.mac_key_file == NULL)) {
+               ha_alert("acme: section '%s': EAB MAC key and key id are 
mutually dependent, specify both or neither.\n", cur_acme->name);
+               err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
+               goto out;
+       }
 
        if (global_ssl.crt_base && *cur_acme->account.file != '/') {
                int rv;
@@ -988,6 +1089,8 @@ void deinit_acme()
                ha_free(&acme_cfgs->challenge);
                ha_free(&acme_cfgs->map);
                ha_free(&acme_cfgs->profile);
+               chunk_destroy(&acme_cfgs->eab.mac_key);
+               ha_free(&acme_cfgs->eab.kid);
 
                free(acme_cfgs);
                acme_cfgs = next;
@@ -1010,6 +1113,8 @@ static struct cfg_kw_list cfg_kws_acme = {ILH, {
        { CFG_ACME, "challenge-ready",  cfg_parse_acme_kws },
        { CFG_ACME, "dns-delay",  cfg_parse_acme_kws },
        { CFG_ACME, "dns-timeout",  cfg_parse_acme_kws },
+       { CFG_ACME, "eab-key-id",  cfg_parse_acme_kws },
+       { CFG_ACME, "eab-mac-key",  cfg_parse_acme_kws },
        { CFG_ACME, "acme-vars",  cfg_parse_acme_vars_provider },
        { CFG_ACME, "provider-name",  cfg_parse_acme_vars_provider },
        { CFG_GLOBAL, "acme.scheduler", cfg_parse_global_acme_sched },
@@ -1269,6 +1374,46 @@ int acme_jws_payload(struct buffer *req, struct ist 
nonce, struct ist url, EVP_P
        return ret;
 }
 
+int acme_jws_eab_payload(struct ist url, EVP_PKEY *acc_key, struct buffer 
mac_key, char *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_HS256;
+       int ret = 1;
+
+       b64payload = alloc_trash_chunk();
+       b64prot = alloc_trash_chunk();
+       jwk = alloc_trash_chunk();
+       b64sign = alloc_trash_chunk();
+
+       if (!b64payload || !b64prot || !jwk || !b64sign || !output) {
+               memprintf(errmsg, "out of memory");
+               goto error;
+       }
+
+       jwk->data = EVP_PKEY_to_pub_jwk(acc_key, jwk->area, jwk->size);
+
+       b64payload->data = jws_b64_payload(jwk->area, b64payload->area, 
b64payload->size);
+       b64prot->data = jws_b64_protected(alg, kid, NULL, NULL, url.ptr, 
b64prot->area, b64prot->size);
+       b64sign->data = jws_b64_hmac_signature(mac_key.area, mac_key.data, alg, 
b64prot->area, b64payload->area, b64sign->area, b64sign->size);
+       output->data = jws_flattened(b64prot->area, b64payload->area, 
b64sign->area, output->area, output->size);
+
+       if (output->data == 0)
+               goto error;
+
+       ret = 0;
+
+error:
+       free_trash_chunk(b64payload);
+       free_trash_chunk(b64prot);
+       free_trash_chunk(jwk);
+       free_trash_chunk(b64sign);
+
+       return ret;
+}
+
 /*
  * Update every certificate instances for the new store
  *
@@ -2211,20 +2356,28 @@ int acme_req_account(struct task *task, struct acme_ctx 
*ctx, int newaccount, ch
 {
        struct buffer *req_in = NULL;
        struct buffer *req_out = NULL;
+       struct buffer *eab_req_out = NULL;
        const struct http_hdr hdrs[] = {
                { IST("Content-Type"), IST("application/jose+json") },
                { IST_NULL, IST_NULL }
        };
        int ret = 1;
 
-        if ((req_in = alloc_trash_chunk()) == NULL)
+       if ((req_in = alloc_trash_chunk()) == NULL)
                goto error;
-        if ((req_out = alloc_trash_chunk()) == NULL)
+       if ((req_out = alloc_trash_chunk()) == NULL)
+               goto error;
+       if ((eab_req_out = alloc_trash_chunk()) == NULL)
                goto error;
 
        if (newaccount) {
                chunk_appendf(req_in, "{");
-               if (ctx->cfg->account.contact != NULL)
+               if (ctx->cfg->eab.mac_key.data > 0 && ctx->cfg->eab.kid != 
NULL) {
+                       if (acme_jws_eab_payload(ctx->resources.newAccount, 
ctx->cfg->account.pkey, ctx->cfg->eab.mac_key, ctx->cfg->eab.kid, eab_req_out, 
errmsg) != 0)
+                               goto out;
+                       chunk_appendf(req_in, "\"externalAccountBinding\": 
%.*s,", (int)eab_req_out->data, eab_req_out->area);
+               }
+               if (ctx->cfg->account.contact)
                        chunk_appendf(req_in, "\"contact\": [ \"mailto:%s\"; 
],", ctx->cfg->account.contact);
                chunk_appendf(req_in, "\"termsOfServiceAgreed\": true");
                chunk_appendf(req_in, "}");
@@ -2246,6 +2399,7 @@ int acme_req_account(struct task *task, struct acme_ctx 
*ctx, int newaccount, ch
 out:
        free_trash_chunk(req_in);
        free_trash_chunk(req_out);
+       free_trash_chunk(eab_req_out);
 
        return ret;
 }
-- 
2.53.0


Reply via email to