Index | Thread | Search

From:
Mischa <bsdnl@mlst.nl>
Subject:
Re: Relayd doesn't like ecdsa
To:
Omar Polo <op@omarpolo.com>
Cc:
Theo Buehler <tb@theobuehler.org>, Tech <tech@openbsd.org>
Date:
Sun, 26 Apr 2026 09:36:08 +0200

Download raw body.

Thread
Hi Omar!

On 2026-04-25 19:10, Omar Polo wrote:
> 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 =)

Of course!! Going to give it a spin today.


> 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;