Index | Thread | Search

From:
Florian Obser <florian@openbsd.org>
Subject:
Re: acme-client: support for external account binding
To:
Jonathan Matthew <jonathan@d14n.org>
Cc:
tech@openbsd.org, sthen@openbsd.org
Date:
Sat, 02 May 2026 19:09:37 +0200

Download raw body.

Thread
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.