Download raw body.
acme-client: support for external account binding
On 2026-05-01 14:09 +10, Jonathan Matthew <jonathan@d14n.org> wrote:
> The ACME protocol includes a scheme allowing a client to bind an ACME
> account key with a account in some non-ACME ("external") system run by
> the CA. This is described in section 7.3.4 of RFC 8555. In short, the
> CA gives you a key out-of-band and your ACME client HMACs your account
> details with that key and sends that to the ACME server.
I know some of these words! My Joo Janta 200 conveniently turned black
when looking at the acctproc changes. That stuff is certainly over my
head. In the past tb@ pointed out the errors of my ways, maybe we can
trick him into reviewing those bits.
>
> The CA we use at work does ACME, but it requires this external account
> binding before it will do anything, so I've implemented support for it in
> acme-client in the diff below.
>
> Since it's only part of the new account process, when you're registering your
> account key with the ACME service, I've made this a command line argument
> rather than something you put in the config file. It's something you'd do
> during initial setup of a new system, so I'm not really concerned about
> exposing the key details to untrusted users on the system. Other clients
> don't seem to be either.
>
> While building the new account request, the net process asks the account
> process to generate the binding JWS using the CA-provided key, includes
> that in the request, and then asks the account process to sign the whole
> thing. Seems a bit fiddly but, to my mind, better than having the account
> process add it into the new account request itself.
>
> Sadly this adds yet another base64 decoder implementation, because there
> wasn't one using the base64url encoding that acme-client could get to.
> This one is based on the libc bcrypt implementation, because it's short and
> uncomplicated and doesn't care about padding.
>
> I've tested this against pebble and digicert's acme service, and
> the existing regress tests pass. I can also add regress test coverage
> for this feature later.
That would be neat.
>
> ok?
>
Two more things inline...
> Index: acctproc.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/acctproc.c,v
> diff -u -p -u -p -r1.32 acctproc.c
> --- acctproc.c 29 Aug 2023 14:44:53 -0000 1.32
> +++ acctproc.c 30 Apr 2026 05:10:56 -0000
> @@ -28,6 +28,7 @@
> #include <openssl/bn.h>
> #include <openssl/ec.h>
> #include <openssl/evp.h>
> +#include <openssl/hmac.h>
> #include <openssl/rsa.h>
> #include <openssl/err.h>
>
> @@ -182,18 +183,13 @@ out:
> }
>
> static int
> -op_sign_rsa(char **prot, EVP_PKEY *pkey, const char *nonce, const char *url)
> +op_sign_rsa(char **jwk, EVP_PKEY *pkey)
> {
> char *exp = NULL, *mod = NULL;
> int rc = 0;
> RSA *r;
>
> - *prot = NULL;
> -
> - /*
> - * First, extract relevant portions of our private key.
> - * Finally, format the header combined with the nonce.
> - */
> + *jwk = NULL;
>
> if ((r = EVP_PKEY_get0_RSA(pkey)) == NULL)
> warnx("EVP_PKEY_get0_RSA");
> @@ -201,8 +197,8 @@ op_sign_rsa(char **prot, EVP_PKEY *pkey,
> warnx("bn2string");
> else if ((exp = bn2string(RSA_get0_e(r))) == NULL)
> warnx("bn2string");
> - else if ((*prot = json_fmt_protected_rsa(exp, mod, nonce, url)) == NULL)
> - warnx("json_fmt_protected_rsa");
> + else if ((*jwk = json_fmt_jwk_rsa(exp, mod)) == NULL)
> + warnx("json_fmt_jwk_rsa");
> else
> rc = 1;
>
> @@ -212,14 +208,14 @@ op_sign_rsa(char **prot, EVP_PKEY *pkey,
> }
>
> static int
> -op_sign_ec(char **prot, EVP_PKEY *pkey, const char *nonce, const char *url)
> +op_sign_ec(char **jwk, EVP_PKEY *pkey)
> {
> BIGNUM *X = NULL, *Y = NULL;
> EC_KEY *ec = NULL;
> char *x = NULL, *y = NULL;
> int rc = 0;
>
> - *prot = NULL;
> + *jwk = NULL;
>
> if ((ec = EVP_PKEY_get0_EC_KEY(pkey)) == NULL)
> warnx("EVP_PKEY_get0_EC_KEY");
> @@ -234,8 +230,8 @@ op_sign_ec(char **prot, EVP_PKEY *pkey,
> warnx("bn2string");
> else if ((y = bn2string(Y)) == NULL)
> warnx("bn2string");
> - else if ((*prot = json_fmt_protected_ec(x, y, nonce, url)) == NULL)
> - warnx("json_fmt_protected_ec");
> + else if ((*jwk = json_fmt_jwk_ec(x, y)) == NULL)
> + warnx("json_fmt_jwk_ec");
> else
> rc = 1;
>
> @@ -262,6 +258,7 @@ op_sign(int fd, EVP_PKEY *pkey, enum acc
> char *prot = NULL, *prot64 = NULL;
> char *sign = NULL, *dig64 = NULL, *fin = NULL;
> char *url = NULL, *kid = NULL, *alg = NULL;
> + char *jwk = NULL;
> const unsigned char *digp;
> unsigned char *dig = NULL, *buf = NULL;
> size_t digsz;
> @@ -309,17 +306,21 @@ op_sign(int fd, EVP_PKEY *pkey, enum acc
> } else {
> switch (EVP_PKEY_base_id(pkey)) {
> case EVP_PKEY_RSA:
> - if (!op_sign_rsa(&prot, pkey, nonce, url))
> + if (!op_sign_rsa(&jwk, pkey))
> goto out;
> break;
> case EVP_PKEY_EC:
> - if (!op_sign_ec(&prot, pkey, nonce, url))
> + if (!op_sign_ec(&jwk, pkey))
> goto out;
> break;
> default:
> warnx("EVP_PKEY_base_id");
> goto out;
> }
> +
> + prot = json_fmt_protected_jwk(alg, jwk, nonce, url);
> + if (prot == NULL)
> + goto out;
> }
>
> /* The header combined with the nonce, base64. */
> @@ -434,6 +435,7 @@ out:
> free(pay64);
> free(url);
> free(nonce);
> + free(jwk);
> free(kid);
> free(prot);
> free(prot64);
> @@ -444,8 +446,94 @@ out:
> return rc;
> }
>
> +static int
> +op_eab(int fd, EVP_PKEY *pkey, const char *eab_kid, const unsigned char *eab_key,
> + int eab_key_len)
> +{
> + char *url = NULL;
> + char *prot = NULL, *prot64 = NULL;
> + char *jwk = NULL, *pay64 = NULL;
> + char *sign = NULL;
> + char *dig64 = NULL, *fin = NULL;
> + unsigned char *dig = NULL;
> + unsigned int digsz;
> + int sign_len, rc = 0;
> +
> + if ((url = readstr(fd, COMM_URL)) == NULL)
> + goto out;
> +
> + /* protected component: algorithm, EAB key ID, new account URL */
> + if ((prot = json_fmt_protected_eab(eab_kid, url)) == NULL) {
> + warnx("json_fmt_protected_eab");
> + goto out;
> + }
> +
> + if ((prot64 = base64buf_url(prot, strlen(prot))) == NULL) {
> + warnx("base64buf_url");
> + goto out;
> + }
> +
> + /* payload component: account public key */
> + switch (EVP_PKEY_base_id(pkey)) {
> + case EVP_PKEY_RSA:
> + if (!op_sign_rsa(&jwk, pkey))
> + goto out;
> + break;
> + case EVP_PKEY_EC:
> + if (!op_sign_ec(&jwk, pkey))
> + goto out;
> + break;
> + default:
> + warnx("EVP_PKEY_base_id");
> + goto out;
> + }
> +
> + if ((pay64 = base64buf_url(jwk, strlen(jwk))) == NULL) {
> + warnx("base64buf_url");
> + goto out;
> + }
> +
> + sign_len = asprintf(&sign, "%s.%s", prot64, pay64);
> + if (sign_len == -1) {
> + warn("asprintf");
> + sign = NULL;
> + goto out;
> + }
> +
> + /* sign with the EAB key */
> + dig = malloc(eab_key_len);
> + HMAC(EVP_sha256(), eab_key, eab_key_len, sign, sign_len, dig, &digsz);
> +
> + if ((dig64 = base64buf_url((char *)dig, digsz)) == NULL) {
> + warnx("base64buf_url");
> + goto out;
> + }
> +
> + if ((fin = json_fmt_signed(prot64, pay64, dig64)) == NULL) {
> + warnx("json_fmt_signed");
> + goto out;
> + } else if (writestr(fd, COMM_REQ, fin) < 0) {
> + goto out;
> + }
> +
> + rc = 1;
> +out:
> +
> + free(fin);
> + free(dig64);
> + free(dig);
> + free(sign);
> + free(pay64);
> + free(jwk);
> + free(prot64);
> + free(prot);
> + free(url);
> + return rc;
> +}
> +
> int
> -acctproc(int netsock, const char *acctkey, enum keytype keytype)
> +acctproc(int netsock, const char *acctkey, enum keytype keytype,
> + const char *eab_kid, const unsigned char *eab_key, int eab_key_len)
> {
> FILE *f = NULL;
> EVP_PKEY *pkey = NULL;
> @@ -522,7 +610,7 @@ acctproc(int netsock, const char *acctke
> if ((lval = readop(netsock, COMM_ACCT)) == 0)
> op = ACCT_STOP;
> else if (lval == ACCT_SIGN || lval == ACCT_KID_SIGN ||
> - lval == ACCT_THUMBPRINT)
> + lval == ACCT_THUMBPRINT || lval == ACCT_EAB)
> op = lval;
>
> if (ACCT__MAX == op) {
> @@ -542,6 +630,12 @@ acctproc(int netsock, const char *acctke
> if (op_thumbprint(netsock, pkey))
> break;
> warnx("op_thumbprint");
> + goto out;
> + case ACCT_EAB:
> + if (op_eab(netsock, pkey, eab_kid, eab_key,
> + eab_key_len))
> + break;
> + warnx("op_eab");
> goto out;
> default:
> abort();
> Index: acme-client.1
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/acme-client.1,v
> diff -u -p -u -p -r1.43 acme-client.1
> --- acme-client.1 18 Sep 2025 14:13:27 -0000 1.43
> +++ acme-client.1 30 Apr 2026 05:10:56 -0000
> @@ -67,10 +67,25 @@ location "/.well-known/acme-challenge/*"
> }
> .Ed
> .Pp
> +Some signing authorities require the client to perform external account
> +binding to associate the ACME account key generated by
> +.Nm
> +with an existing account in a non-ACME system such as a CA customer database.
> +To enable ACME account binding, the CA operating the ACME server needs to provide
> +the ACME client with a MAC key and a key identifier, using some mechanism outside
> +of ACME.
> +These can be supplied to
> +.Nm
> +using the
> +.Fl e
> +command line argument.
> +.Pp
> The options are as follows:
> .Bl -tag -width Ds
> .It Fl F
> Force certificate renewal, regardless of remaining lifetime.
> +.It Fl e Ar key-id:key
> +Specify MAC key and key identifier for external account binding.
could you be more explicit how a key-id and key actually look like?
> .It Fl f Ar configfile
> Specify an alternative configuration file.
> .It Fl n
> Index: base64.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/base64.c,v
> diff -u -p -u -p -r1.9 base64.c
> --- base64.c 24 Jan 2017 13:32:55 -0000 1.9
> +++ base64.c 30 Apr 2026 05:10:56 -0000
> @@ -19,6 +19,7 @@
> #include <resolv.h>
>
> #include <stdlib.h>
> +#include <string.h>
>
> #include "extern.h"
>
> @@ -64,4 +65,70 @@ base64buf_url(const char *data, size_t l
> }
>
> return buf;
> +}
> +
> +static const u_int8_t index_64[128] = {
> + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
> + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
> + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
> + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
> + 255, 255, 255, 255, 255, 62, 255, 255, 52, 53,
> + 54, 55, 56, 57, 58, 59, 60, 61, 255, 255,
> + 255, 255, 255, 255, 255, 0, 1, 2, 3, 4,
> + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
> + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
> + 25, 255, 255, 255, 255, 63, 255, 26, 27, 28,
> + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
> + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
> + 49, 50, 51, 255, 255, 255, 255, 255
> +};
> +#define CHAR64(c) ( (c) > 127 ? 255 : index_64[(c)])
> +
> +int
> +unbase64buf_url(const unsigned char *data, unsigned char **decoded)
> +{
> + unsigned char *out, *bp;
> + const unsigned char *p;
> + unsigned char c1, c2, c3, c4;
> + int len;
> +
> + len = strlen(data);
> + out = malloc(len);
> + if (out == NULL)
> + return -1;
> +
> + p = data;
> + bp = out;
> + while (p < data + len) {
> + c1 = CHAR64(*p);
> + /* Invalid data */
> + if (c1 == 255)
> + return -1;
> +
> + c2 = CHAR64(*(p + 1));
> + if (c2 == 255)
> + return -1;
> +
> + *bp++ = (c1 << 2) | ((c2 & 0x30) >> 4);
> + if ((p + 2) >= data + len)
> + break;
> +
> + c3 = CHAR64(*(p + 2));
> + if (c3 == 255)
> + return -1;
> +
> + *bp++ = ((c2 & 0x0f) << 4) | ((c3 & 0x3c) >> 2);
> + if ((p + 3) >= data + len)
> + break;
> +
> + c4 = CHAR64(*(p + 3));
> + if (c4 == 255)
> + return -1;
> + *bp++ = ((c3 & 0x03) << 6) | c4;
> +
> + p += 4;
> + }
> +
> + *decoded = out;
> + return (bp - out);
> }
> Index: extern.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/extern.h,v
> diff -u -p -u -p -r1.23 extern.h
> --- extern.h 23 Feb 2026 10:27:49 -0000 1.23
> +++ extern.h 30 Apr 2026 05:10:56 -0000
> @@ -34,6 +34,7 @@ enum acctop {
> ACCT_SIGN,
> ACCT_KID_SIGN,
> ACCT_THUMBPRINT,
> + ACCT_EAB,
> ACCT__MAX
> };
>
> @@ -200,7 +201,8 @@ __BEGIN_DECLS
> * Start with our components.
> * These are all isolated and talk to each other using sockets.
> */
> -int acctproc(int, const char *, enum keytype);
> +int acctproc(int, const char *, enum keytype, const char *,
> + const unsigned char *, int);
> int certproc(int, int);
> int chngproc(int, const char *);
> int dnsproc(int);
> @@ -209,7 +211,7 @@ int fileproc(int, const char *, const
> const char *);
> int keyproc(int, struct domain_c *);
> int netproc(int, int, int, int, int, int, int,
> - struct authority_c *, struct domain_c *);
> + struct authority_c *, struct domain_c *, int);
>
> /*
> * Debugging functions.
> @@ -240,6 +242,7 @@ int checkexit_ext(int *, pid_t, enum c
> */
> size_t base64len(size_t);
> char *base64buf_url(const char *, size_t);
> +int unbase64buf_url(const unsigned char *, unsigned char **);
>
> /*
> * JSON parsing routines.
> @@ -259,18 +262,19 @@ char *json_getstr(struct jsmnn *, const
>
> char *json_fmt_newcert(const char *);
> char *json_fmt_chkacc(void);
> -char *json_fmt_newacc(const char *);
> +char *json_fmt_newacc(const char *, const char *);
> char *json_fmt_neworder(struct domain_c *);
> -char *json_fmt_protected_rsa(const char *,
> - const char *, const char *, const char *);
> -char *json_fmt_protected_ec(const char *, const char *, const char *,
> - const char *);
> -char *json_fmt_protected_kid(const char*, const char *, const char *,
> - const char *);
> +char *json_fmt_jwk_rsa(const char *, const char *);
> +char *json_fmt_jwk_ec(const char *, const char *);
> +char *json_fmt_protected_jwk(const char *, const char *,
> + const char *, const char *);
> +char *json_fmt_protected_kid(const char *, const char *,
> + const char *, const char *);
> char *json_fmt_revokecert(const char *);
> char *json_fmt_thumb_rsa(const char *, const char *);
> char *json_fmt_thumb_ec(const char *, const char *);
> char *json_fmt_signed(const char *, const char *, const char *);
> +char *json_fmt_protected_eab(const char *, const char *);
>
> /*
> * Should we print debugging messages?
> Index: json.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/json.c,v
> diff -u -p -u -p -r1.23 json.c
> --- json.c 23 Feb 2026 10:27:49 -0000 1.23
> +++ json.c 30 Apr 2026 05:10:56 -0000
> @@ -618,7 +618,7 @@ json_fmt_chkacc(void)
> * Format the "newAccount" resource request.
> */
> char *
> -json_fmt_newacc(const char *contact)
> +json_fmt_newacc(const char *contact, const char *eab)
> {
> int c;
> char *p, *cnt = NULL;
> @@ -630,6 +630,14 @@ json_fmt_newacc(const char *contact)
> return NULL;
> }
> }
> + if (eab != NULL) {
> + char *ecnt = NULL;
> + c = asprintf(&ecnt, "%s\"externalAccountBinding\": %s, ",
> + cnt == NULL ? "" : cnt, eab);
> +
> + free(cnt);
> + cnt = ecnt;
> + }
>
> c = asprintf(&p, "{"
> "%s"
> @@ -737,23 +745,35 @@ json_fmt_newcert(const char *cert)
> }
>
> /*
> - * Protected component of json_fmt_signed().
> + * Format an RSA public key in JWK format.
> */
> char *
> -json_fmt_protected_rsa(const char *exp, const char *mod, const char *nce,
> - const char *url)
> +json_fmt_jwk_rsa(const char *exp, const char *mod)
> {
> int c;
> char *p;
>
> - c = asprintf(&p, "{"
> - "\"alg\": \"RS256\", "
> - "\"jwk\": "
> - "{\"e\": \"%s\", \"kty\": \"RSA\", \"n\": \"%s\"}, "
> - "\"nonce\": \"%s\", "
> - "\"url\": \"%s\""
> - "}",
> - exp, mod, nce, url);
> + c = asprintf(&p, "{\"e\": \"%s\", \"kty\": \"RSA\", \"n\": \"%s\"}",
> + exp, mod);
> + if (c == -1) {
> + warn("asprintf");
> + p = NULL;
> + }
> + return p;
> +}
> +
> +/*
> + * Format an EC public key in JWK format.
> + */
> +char *
> +json_fmt_jwk_ec(const char *x, const char *y)
> +{
> + int c;
> + char *p;
> +
> + c = asprintf(&p, "{\"crv\": \"P-384\", \"kty\": \"EC\","
> + " \"x\": \"%s\", \"y\": \"%s\"}",
> + x, y);
> if (c == -1) {
> warn("asprintf");
> p = NULL;
> @@ -765,19 +785,19 @@ json_fmt_protected_rsa(const char *exp,
> * Protected component of json_fmt_signed().
> */
> char *
> -json_fmt_protected_ec(const char *x, const char *y, const char *nce,
> +json_fmt_protected_jwk(const char *alg, const char *jwk, const char *nce,
> const char *url)
> {
> int c;
> char *p;
>
> c = asprintf(&p, "{"
> - "\"alg\": \"ES384\", "
> - "\"jwk\": "
> - "{\"crv\": \"P-384\", \"kty\": \"EC\", \"x\": \"%s\", "
> - "\"y\": \"%s\"}, \"nonce\": \"%s\", \"url\": \"%s\""
> + "\"alg\": \"%s\", "
> + "\"jwk\": %s, "
> + "\"nonce\": \"%s\", "
> + "\"url\": \"%s\""
> "}",
> - x, y, nce, url);
> + alg, jwk, nce, url);
> if (c == -1) {
> warn("asprintf");
> p = NULL;
> @@ -871,6 +891,28 @@ json_fmt_thumb_ec(const char *x, const c
> c = asprintf(&p, "{\"crv\":\"P-384\",\"kty\":\"EC\",\"x\":\"%s\","
> "\"y\":\"%s\"}",
> x, y);
> + if (c == -1) {
> + warn("asprintf");
> + p = NULL;
> + }
> + return p;
> +}
> +
> +/*
> + * Protected component of external account binding.
> + */
> +char *
> +json_fmt_protected_eab(const char *keyid, const char *url)
> +{
> + int c;
> + char *p;
> +
> + c = asprintf(&p, "{"
> + "\"alg\": \"HS256\", "
> + "\"kid\": \"%s\", "
> + "\"url\": \"%s\""
> + "}",
> + keyid, url);
> if (c == -1) {
> warn("asprintf");
> p = NULL;
> Index: main.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/main.c,v
> diff -u -p -u -p -r1.58 main.c
> --- main.c 23 Feb 2026 10:27:49 -0000 1.58
> +++ main.c 30 Apr 2026 05:10:56 -0000
> @@ -43,12 +43,14 @@ main(int argc, char *argv[])
> char *certdir = NULL;
> char *chngdir = NULL, *auth = NULL;
> char *conffile = CONF_FILE;
> + char *eab = NULL;
> char *tmps, *tmpsd;
> + unsigned char *eab_key = NULL;
> int key_fds[2], acct_fds[2], chng_fds[2], cert_fds[2];
> int file_fds[2], dns_fds[2], rvk_fds[2];
> int force = 0;
> int c, rc, revocate = 0;
> - int popts = 0;
> + int popts = 0, i, eab_len, eab_key_len;
> pid_t pids[COMP__MAX];
> size_t ne;
>
> @@ -60,11 +62,14 @@ main(int argc, char *argv[])
> if (setlocale(LC_CTYPE, "C") == NULL)
> errx(1, "setlocale");
>
> - while ((c = getopt(argc, argv, "Fnrvf:")) != -1)
> + while ((c = getopt(argc, argv, "Fe:k:nrvf:")) != -1)
> switch (c) {
> case 'F':
> force = 1;
> break;
> + case 'e':
> + eab = strdup(optarg);
> + break;
> case 'f':
> if ((conffile = strdup(optarg)) == NULL)
> err(EXIT_FAILURE, "strdup");
> @@ -166,6 +171,27 @@ main(int argc, char *argv[])
> ne++;
> }
>
> + if (eab != NULL) {
> + eab_len = strlen(eab);
> + for (i = 0; i < eab_len; i++) {
> + if (eab[i] == ':') {
> + eab[i] = '\0';
> + break;
> + }
> + }
this is a weird spelling of strsep(3), isn't it?
> +
> + if (i == eab_len) {
> + warnx("EAB data must be in the format keyid:key");
> + ne++;
> + }
> +
> + eab_key_len = unbase64buf_url((unsigned char *)eab + i + 1, &eab_key);
> + if (eab_key_len == -1) {
> + warnx("unable to decode EAB key");
> + ne++;
> + }
> + }
> +
> if (ne > 0)
> return EXIT_FAILURE;
>
> @@ -222,7 +248,7 @@ main(int argc, char *argv[])
> c = netproc(key_fds[1], acct_fds[1],
> chng_fds[1], cert_fds[1],
> dns_fds[1], rvk_fds[1],
> - revocate, authority, domain);
> + revocate, authority, domain, eab != NULL);
> exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
> }
>
> @@ -267,7 +293,7 @@ main(int argc, char *argv[])
> close(file_fds[0]);
> close(file_fds[1]);
> c = acctproc(acct_fds[0], authority->account,
> - authority->keytype);
> + authority->keytype, eab, eab_key, eab_key_len);
> exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
> }
>
> @@ -378,6 +404,6 @@ main(int argc, char *argv[])
> return rc != COMP__MAX ? EXIT_FAILURE : (c == 2 ? EXIT_SUCCESS : 2);
> usage:
> fprintf(stderr,
> - "usage: acme-client [-Fnrv] [-f configfile] handle\n");
> + "usage: acme-client [-Fnrv] [-f configfile] [-e kid:key] handle\n");
> return EXIT_FAILURE;
> }
> Index: netproc.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/netproc.c,v
> diff -u -p -u -p -r1.46 netproc.c
> --- netproc.c 23 Feb 2026 10:27:49 -0000 1.46
> +++ netproc.c 30 Apr 2026 05:10:56 -0000
> @@ -408,14 +408,30 @@ sreq(struct conn *c, const char *addr, i
> * Returns non-zero on success.
> */
> static int
> -donewacc(struct conn *c, const struct capaths *p, const char *contact)
> +donewacc(struct conn *c, const struct capaths *p, const char *contact,
> + int eab)
> {
> struct jsmnn *j = NULL;
> int rc = 0;
> char *req, *detail, *error = NULL, *accturi = NULL;
> + char *eab_json = NULL;
> long lc;
>
> - if ((req = json_fmt_newacc(contact)) == NULL)
> + if (eab) {
> + /* ask acct proc to produce eab json */
> + if (writeop(c->fd, COMM_ACCT, ACCT_EAB) <= 0) {
> + return -1;
> + } else if (writestr(c->fd, COMM_URL, p->newaccount) <= 0) {
> + return -1;
> + }
> +
> + /* Now read back the signed payload. */
> + if ((eab_json = readstr(c->fd, COMM_REQ)) == NULL) {
> + return -1;
> + }
> + }
> +
> + if ((req = json_fmt_newacc(contact, eab_json)) == NULL)
> warnx("json_fmt_newacc");
> else if ((lc = sreq(c, p->newaccount, 0, req, &c->kid)) < 0)
> warnx("%s: bad comm", p->newaccount);
> @@ -455,7 +471,8 @@ donewacc(struct conn *c, const struct ca
> * Returns non-zero on success.
> */
> static int
> -dochkacc(struct conn *c, const struct capaths *p, const char *contact)
> +dochkacc(struct conn *c, const struct capaths *p, const char *contact,
> + int eab)
> {
> int rc = 0;
> char *req, *accturi = NULL;
> @@ -470,10 +487,12 @@ dochkacc(struct conn *c, const struct ca
> else if (c->buf.buf == NULL || c->buf.sz == 0)
> warnx("%s: empty response", p->newaccount);
> else if (lc == 400)
> - rc = donewacc(c, p, contact);
> + rc = donewacc(c, p, contact, eab);
> else
> rc = 1;
>
> + rc = donewacc(c, p, contact, eab);
> +
> if (c->kid == NULL)
> rc = 0;
> else {
> @@ -723,7 +742,7 @@ dodirs(struct conn *c, const char *addr,
> int
> netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd,
> int revocate, struct authority_c *authority,
> - struct domain_c *domain)
> + struct domain_c *domain, int eab)
> {
> int rc = 0, retries = 0;
> size_t i;
> @@ -805,7 +824,7 @@ netproc(int kfd, int afd, int Cfd, int c
> c.newnonce = paths.newnonce;
>
> /* Check if our account already exists or create it. */
> - if (!dochkacc(&c, &paths, authority->contact))
> + if (!dochkacc(&c, &paths, authority->contact, eab))
> goto out;
>
> /*
>
--
In my defence, I have been left unsupervised.
acme-client: support for external account binding