Index | Thread | Search

From:
"Omar Polo" <op@omarpolo.com>
Subject:
Re: Relayd doesn't like ecdsa
To:
Mischa <bsdnl@mlst.nl>
Cc:
Theo Buehler <tb@theobuehler.org>, Tech <tech@openbsd.org>
Date:
Sat, 25 Apr 2026 19:10:42 +0200

Download raw body.

Thread
Hello,

Mischa <bsdnl@mlst.nl> wrote:
> On 2026-04-23 14:25, Theo Buehler wrote:
> > On Thu, Apr 23, 2026 at 02:07:45PM +0200, Mischa wrote:
> >> Hi All,
> >> 
> >> When using edcsa within acme-client.conf, relayd is unable to use the
> >> key/cert, it seems to be looking for an RSA key/cert specifically. Is 
> >> there
> >> a way to go around this?
> > 
> > No. The privsep stuff has only RSA wired up. Someone motivated could
> > probably crib from smtpd's ca.c.
> 
> I wish I had the skilzzz. :/
> Willing to incentivize where possible. :)

some time ago while working on smtpd's ca.c I wrote an implementation
for relayd, mostly to validate my understanding.  I was too scared to
share it, I don't use relayd normally, and I try to stay a little bit
away from it in general.  (sorry, I found it confusing!)

Anyway, I tried to resurrect the diff.  It works for me with a stupid
small config and an ec key generated with:

	key=...
	pem=...
	openssl ecparam -name secp384r1 -genkey -noout -out "${key}"
	openssl req -new -x509 -key "${key}" -out "${pem}" -days 365 \
		-nodes -subj "/CN=localhost"
				
can you give it a spin?  there are chances it might work =)

I don't like how we reuse the cko struct in ca_dispatch_relay(), but
that's what was already done in the RSA case.

diff /usr/src
path + /usr/src
commit - e268a8ba09fa63295bce8b5f024a710203085e2a
blob - c4f527fa96f5b64a702d682f9d59ef92cf46d9d4
file + usr.sbin/relayd/ca.c
--- usr.sbin/relayd/ca.c
+++ usr.sbin/relayd/ca.c
@@ -222,9 +222,11 @@ ca_dispatch_relay(int fd, struct privsep_proc *p, stru
 	struct ctl_keyop	 cko;
 	EVP_PKEY		*pkey;
 	RSA			*rsa;
+	EC_KEY			*ecdsa;
 	u_char			*from = NULL, *to = NULL;
 	struct iovec		 iov[2];
-	int			 c = 0;
+	int			 ret, c = 0;
+	unsigned int		 len;
 
 	switch (imsg->hdr.type) {
 	case IMSG_CA_PRIVENC:
@@ -291,6 +293,55 @@ ca_dispatch_relay(int fd, struct privsep_proc *p, stru
 		free(to);
 		RSA_free(rsa);
 		break;
+
+	case IMSG_CA_ECDSA_SIGN:
+		IMSG_SIZE_CHECK(imsg, (&cko));
+		bcopy(imsg->data, &cko, sizeof(cko));
+		if (cko.cko_proc > env->sc_conf.prefork_relay)
+			fatalx("%s: invalid relay proc", __func__);
+		if (IMSG_DATA_SIZE(imsg) != (sizeof(cko) + cko.cko_flen))
+			fatalx("%s: invalid key operation", __func__);
+		from = (u_char *)imsg->data + sizeof(cko);
+
+		if ((pkey = pkey_find(env, cko.cko_hash)) == NULL) {
+			log_warnx("%s: invalid relay hash '%s'",
+			    __func__, cko.cko_hash);
+			/* Signal failure to the waiting relay worker. */
+			cko.cko_tlen = -1;
+			iov[c].iov_base = &cko;
+			iov[c++].iov_len = sizeof(cko);
+			if (proc_composev_imsg(env->sc_ps, PROC_RELAY,
+			    cko.cko_proc, imsg->hdr.type, -1, -1, iov,
+			     c) == -1)
+				log_warn("%s: proc_composev_imsg", __func__);
+			break;
+		}
+
+		if ((ecdsa = EVP_PKEY_get1_EC_KEY(pkey)) == NULL)
+			fatalx("%s: invalid relay key", __func__);
+
+		len = ECDSA_size(ecdsa);
+		if ((to = calloc(1, len)) == NULL)
+			fatalx("ca_imsg: calloc");
+		ret = ECDSA_sign(0, from, cko.cko_flen,
+		    to, &len, ecdsa);
+
+		iov[c].iov_base = &cko;
+		iov[c++].iov_len = sizeof(cko);
+		if (ret > 0) {
+			cko.cko_tlen = len;
+			iov[c].iov_base = to;
+			iov[c++].iov_len = len;
+		}
+
+		if (proc_composev_imsg(env->sc_ps, PROC_RELAY, cko.cko_proc,
+		    imsg->hdr.type, -1, -1, iov, c) == -1)
+			log_warn("%s: proc_composev_imsg", __func__);
+
+		free(to);
+		EC_KEY_free(ecdsa);
+		break;
+
 	default:
 		return -1;
 	}
@@ -440,14 +491,11 @@ rsae_priv_dec(int flen, const u_char *from, u_char *to
 	return rsae_send_imsg(flen, from, to, rsa, padding, IMSG_CA_PRIVDEC);
 }
 
-void
-ca_engine_init(struct relayd *x_env)
+static void
+rsa_engine_init(void)
 {
 	const char	*errstr;
 
-	if (env == NULL)
-		env = x_env;
-
 	if (rsa_default != NULL)
 		return;
 
@@ -477,3 +525,186 @@ ca_engine_init(struct relayd *x_env)
 	RSA_meth_free(rsae_method);
 	fatalx("%s: %s", __func__, errstr);
 }
+
+/*
+ * ECDSA privsep engine (called from unprivileged processes)
+ */
+
+const EC_KEY_METHOD *ecdsa_default = NULL;
+
+static EC_KEY_METHOD *ecdsae_method = NULL;
+
+static ECDSA_SIG *
+ecdsae_send_enc_imsg(const unsigned char *dgst, int dgst_len,
+    const BIGNUM *inv, const BIGNUM *rp, EC_KEY *eckey)
+{
+	struct privsep	*ps = env->sc_ps;
+	struct pollfd	 pfd[1];
+	struct ctl_keyop cko;
+	struct iovec	 iov[2];
+	struct imsgbuf	*ibuf;
+	struct imsgev	*iev;
+	struct imsg	 imsg;
+	int		 n, done = 0, cnt = 0;
+	const u_char	*toptr;
+	static u_int	 seq = 0;
+
+	char		*hash;
+	ECDSA_SIG	*sig = NULL;
+
+	if ((hash = EC_KEY_get_ex_data(eckey, 0)) == NULL)
+		return (0);
+
+	iev = proc_iev(ps, PROC_CA, ps->ps_instance);
+	ibuf = &iev->ibuf;
+
+	/*
+	 * XXX this could be nicer...
+	 */
+
+	memset(&cko, 0, sizeof(cko));
+	(void)strlcpy(cko.cko_hash, hash, sizeof(cko.cko_hash));
+	cko.cko_proc = ps->ps_instance;
+	cko.cko_flen = dgst_len;
+	cko.cko_cookie = seq++;
+
+	iov[cnt].iov_base = &cko;
+	iov[cnt++].iov_len = sizeof(cko);
+	iov[cnt].iov_base = (void *)(uintptr_t)dgst;
+	iov[cnt++].iov_len = dgst_len;
+
+	/*
+	 * Send a synchronous imsg because we cannot defer the ECDSA
+	 * operation in OpenSSL's engine layer.
+	 */
+	if (imsg_composev(ibuf, IMSG_CA_ECDSA_SIGN, 0, 0, -1, iov, cnt) == -1) {
+		log_warn("%s: imsg_composev", __func__);
+		return NULL;
+	}
+	if (imsgbuf_flush(ibuf) == -1) {
+		log_warn("%s: imsgbuf_flush", __func__);
+		return NULL;
+	}
+
+	pfd[0].fd = ibuf->fd;
+	pfd[0].events = POLLIN;
+
+	while (!done) {
+		switch (poll(pfd, 1, RELAY_TLS_PRIV_TIMEOUT)) {
+		case -1:
+			if (errno != EINTR)
+				fatal("%s: poll", __func__);
+			continue;
+		case 0:
+			log_warnx("%s: priv ecdsa poll timeout, keyop #%x",
+			    __func__,
+			    cko.cko_cookie);
+			return NULL;
+		default:
+			break;
+		}
+		if ((n = imsgbuf_read(ibuf)) == -1)
+			fatalx("imsgbuf_read");
+		if (n == 0)
+			fatalx("pipe closed");
+
+		while (!done) {
+			if ((n = imsg_get(ibuf, &imsg)) == -1)
+				fatalx("imsg_get error");
+			if (n == 0)
+				break;
+
+			IMSG_SIZE_CHECK(&imsg, (&cko));
+			memcpy(&cko, imsg.data, sizeof(cko));
+
+			/*
+			 * Due to earlier timed out requests, there may be
+			 * responses that need to be skipped.
+			 */
+			if (cko.cko_cookie != seq - 1) {
+				log_warnx(
+				    "%s: priv ecdsa obsolete keyop #%x",
+				    __func__,
+				    cko.cko_cookie);
+				imsg_free(&imsg);
+				continue;
+			}
+
+			if (imsg.hdr.type != IMSG_CA_ECDSA_SIGN)
+				fatalx("invalid response");
+
+			if (cko.cko_tlen == -1) {
+				log_warnx("%s: priv ecdsa failed for key %s",
+				    __func__, cko.cko_hash);
+			} else if (cko.cko_tlen > 0) {
+				if (IMSG_DATA_SIZE(&imsg) !=
+				    (sizeof(cko) + cko.cko_tlen))
+					fatalx("data size");
+				toptr = (u_char *)imsg.data + sizeof(cko);
+				d2i_ECDSA_SIG(&sig, (const unsigned char **)&toptr,
+				    cko.cko_tlen);
+			}
+			done = 1;
+
+			imsg_free(&imsg);
+		}
+	}
+	imsg_event_add(iev);
+
+	return sig;
+}
+
+static ECDSA_SIG *
+ecdsae_do_sign(const unsigned char *dgst, int dgst_len, const BIGNUM *inv,
+    const BIGNUM *rp, EC_KEY *eckey)
+{
+	ECDSA_SIG *(*psign_sig)(const unsigned char *, int, const BIGNUM *,
+	    const BIGNUM *, EC_KEY *);
+
+	DPRINTF("%s:%d", __func__, __LINE__);
+	if (EC_KEY_get_ex_data(eckey, 0) != NULL)
+		return (ecdsae_send_enc_imsg(dgst, dgst_len, inv, rp, eckey));
+	EC_KEY_METHOD_get_sign(ecdsa_default, NULL, NULL, &psign_sig);
+	return (psign_sig(dgst, dgst_len, inv, rp, eckey));
+}
+
+static void
+ecdsa_engine_init(void)
+{
+	int (*sign)(int, const unsigned char *, int, unsigned char *,
+	    unsigned int *, const BIGNUM *, const BIGNUM *, EC_KEY *);
+	int (*sign_setup)(EC_KEY *, BN_CTX *, BIGNUM **, BIGNUM **);
+	const char *errstr;
+
+	if ((ecdsa_default = EC_KEY_get_default_method()) == NULL) {
+		errstr = "EC_KEY_get_default_method";
+		goto fail;
+	}
+
+	if ((ecdsae_method = EC_KEY_METHOD_new(ecdsa_default)) == NULL) {
+		errstr = "EC_KEY_METHOD_new";
+		goto fail;
+	}
+
+	EC_KEY_METHOD_get_sign(ecdsa_default, &sign, &sign_setup, NULL);
+	EC_KEY_METHOD_set_sign(ecdsae_method, sign, sign_setup,
+	    ecdsae_do_sign);
+
+	EC_KEY_set_default_method(ecdsae_method);
+
+	return;
+
+ fail:
+	ssl_error(errstr);
+	fatalx("%s", errstr);
+}
+
+void
+ca_engine_init(struct relayd *x_env)
+{
+	if (env == NULL)
+		env = x_env;
+
+	rsa_engine_init();
+	ecdsa_engine_init();
+}
commit - e268a8ba09fa63295bce8b5f024a710203085e2a
blob - a5363989f4b4cbcca6c899b12de4ccd3cdf4dfcb
file + usr.sbin/relayd/relayd.h
--- usr.sbin/relayd/relayd.h
+++ usr.sbin/relayd/relayd.h
@@ -1005,6 +1005,7 @@ enum imsg_type {
 	IMSG_CFG_DONE,
 	IMSG_CA_PRIVENC,
 	IMSG_CA_PRIVDEC,
+	IMSG_CA_ECDSA_SIGN,
 	IMSG_SESS_PUBLISH,	/* from relay to pfe */
 	IMSG_SESS_UNPUBLISH,
 	IMSG_TLSTICKET_REKEY
@@ -1289,6 +1290,7 @@ void	 script_done(struct relayd *, struct ctl_script *
 int	 script_exec(struct relayd *, struct ctl_script *);
 
 /* ssl.c */
+void	 ssl_error(const char *);
 char	*ssl_load_key(struct relayd *, const char *, off_t *, char *);
 uint8_t *ssl_update_certificate(const uint8_t *, size_t, EVP_PKEY *,
 	    EVP_PKEY *, X509 *, size_t *);
commit - e268a8ba09fa63295bce8b5f024a710203085e2a
blob - 19950b89e56aec83e15bc9635293bd078f331e45
file + usr.sbin/relayd/ssl.c
--- usr.sbin/relayd/ssl.c
+++ usr.sbin/relayd/ssl.c
@@ -46,6 +46,18 @@ ssl_password_cb(char *buf, int size, int rwflag, void 
 	return (len);
 }
 
+void
+ssl_error(const char *where)
+{
+	unsigned long	code;
+	char		errbuf[128];
+
+	for (; (code = ERR_get_error()) != 0 ;) {
+		ERR_error_string_n(code, errbuf, sizeof(errbuf));
+		log_debug("debug: SSL library error: %s: %s", where, errbuf);
+	}
+}
+
 char *
 ssl_load_key(struct relayd *env, const char *name, off_t *len, char *pass)
 {
@@ -181,6 +193,7 @@ ssl_load_pkey(char *buf, off_t len, X509 **x509ptr, EV
 	X509		*x509 = NULL;
 	EVP_PKEY	*pkey = NULL;
 	RSA		*rsa = NULL;
+	EC_KEY		*eckey = NULL;
 	char		*hash = NULL;
 
 	if ((in = BIO_new_mem_buf(buf, len)) == NULL) {
@@ -196,21 +209,47 @@ ssl_load_pkey(char *buf, off_t len, X509 **x509ptr, EV
 		log_warnx("%s: X509_get_pubkey failed", __func__);
 		goto fail;
 	}
-	if ((rsa = EVP_PKEY_get1_RSA(pkey)) == NULL) {
-		log_warnx("%s: failed to extract RSA", __func__);
-		goto fail;
-	}
+
 	if ((hash = malloc(TLS_CERT_HASH_SIZE)) == NULL) {
 		log_warn("%s: allocate hash failed", __func__);
 		goto fail;
 	}
 	hash_x509(x509, hash, TLS_CERT_HASH_SIZE);
-	if (RSA_set_ex_data(rsa, 0, hash) != 1) {
-		log_warnx("%s: failed to set hash as exdata", __func__);
+
+	switch (EVP_PKEY_id(pkey)) {
+	case EVP_PKEY_RSA:
+		if ((rsa = EVP_PKEY_get1_RSA(pkey)) == NULL) {
+			log_warnx("%s: failed to extract RSA", __func__);
+			goto fail;
+		}
+		if (RSA_set_ex_data(rsa, 0, hash) != 1) {
+			log_warnx("%s: failed to set hash as exdata", __func__);
+			goto fail;
+		}
+		break;
+	case EVP_PKEY_EC:
+		if ((eckey = EVP_PKEY_get1_EC_KEY(pkey)) == NULL) {
+			log_warnx("%s: failed to set extract EC key", __func__);
+			goto fail;
+		}
+		if (EC_KEY_set_ex_data(eckey, 0, hash) == 0) {
+			log_warnx("%s: failed to set hash as exdata", __func__);
+			goto fail;
+		}
+
+		/* Reset the key to work around caching in OpenSSL 3. */
+		if (EVP_PKEY_set1_EC_KEY(pkey, eckey) == 0) {
+			log_warnx("%s: failed to set EC key", __func__);
+			goto fail;
+		}
+		break;
+	default:
+		log_warnx("%s: incorrect key type", __func__);
 		goto fail;
 	}
 
-	RSA_free(rsa); /* dereference, will be cleaned up with pkey */
+	RSA_free(rsa);
+	EC_KEY_free(eckey);
 	*pkeyptr = pkey;
 	if (x509ptr != NULL)
 		*x509ptr = x509;