Index | Thread | Search

From:
Jonathan Matthew <jonathan@d14n.org>
Subject:
Re: acme-client: support for external account binding
To:
Theo Buehler <tb@theobuehler.org>
Cc:
tech@openbsd.org, sthen@openbsd.org, florian@openbsd.org
Date:
Wed, 6 May 2026 15:36:00 +1000

Download raw body.

Thread
On Sat, May 02, 2026 at 07:53:50PM +0200, Theo Buehler wrote:
> On Sat, May 02, 2026 at 07:09:37PM +0200, Florian Obser wrote:
> > 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.
> 
> I haven't checked against the spec, but it looks fine to me, just two
> things:
> 
> > > +	/* sign with the EAB key */
> > > +	dig = malloc(eab_key_len);
> > > +	HMAC(EVP_sha256(), eab_key, eab_key_len, sign, sign_len, dig, &digsz);
> 
> Both malloc and HMAC should be error checked (against 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);
> 
> Pretty sure this should return NULL if c == -1.

Thanks, here's an updated version with comments addressed.


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	6 May 2026 03:58:31 -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,96 @@ 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[EVP_MAX_MD_SIZE];
+	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 */
+	if (HMAC(EVP_sha256(), eab_key, eab_key_len, sign, sign_len, dig,
+	    &digsz) == NULL) {
+		warnx("HMAC");
+		goto out;
+	}
+
+	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(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 +612,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 +632,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	6 May 2026 03:58:31 -0000
@@ -24,6 +24,7 @@
 .Nm acme-client
 .Op Fl Fnrv
 .Op Fl f Ar configfile
+.Op Fl e Ar keyid:key
 .Ar handle
 .Sh DESCRIPTION
 .Nm
@@ -67,10 +68,27 @@ 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 base64url-encoded MAC key and a key identifier
+string, 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 external account binding parameters.
+This consists of the key identifier string and the base64url-encoded MAC key
+separated by a colon.
 .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	6 May 2026 03:58:31 -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	6 May 2026 03:58:31 -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	6 May 2026 03:58:31 -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,18 @@ json_fmt_newacc(const char *contact)
 			return NULL;
 		}
 	}
+	if (eab != NULL) {
+		char *ecnt = NULL;
+		c = asprintf(&ecnt, "%s\"externalAccountBinding\": %s, ",
+		    cnt == NULL ? "" : cnt, eab);
+		if (c == -1) {
+			warn("asprintf");
+			return NULL;
+		}
+
+		free(cnt);
+		cnt = ecnt;
+	}
 
 	c = asprintf(&p, "{"
 	    "%s"
@@ -737,23 +749,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 +789,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 +895,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	6 May 2026 03:58:31 -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, *eab_key_enc = 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, 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: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,22 @@ main(int argc, char *argv[])
 		ne++;
 	}
 
+	if (eab != NULL) {
+		eab_key_enc = eab;
+		eab = strsep(&eab_key_enc, ":");
+		if (eab_key_enc == NULL) {
+			warnx("EAB parameters must be in the format keyid:key");
+			ne++;
+		} else {
+			eab_key_len = unbase64buf_url(
+			    (unsigned char *)eab_key_enc, &eab_key);
+			if (eab_key_len == -1) {
+				warnx("unable to decode EAB key");
+				ne++;
+			}
+		}
+	}
+
 	if (ne > 0)
 		return EXIT_FAILURE;
 
@@ -222,7 +243,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 +288,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 +399,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	6 May 2026 03:58:31 -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,7 +487,7 @@ 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;
 
@@ -723,7 +740,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 +822,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;
 
 	/*