From: Mischa Subject: Re: Relayd doesn't like ecdsa To: Omar Polo Cc: Theo Buehler , Tech Date: Sun, 26 Apr 2026 10:06:45 +0200 On 2026-04-25 19:10, Omar Polo wrote: > Hello, > > Mischa 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 =) That seems to work. https://internet.nl/site/current.openbsd.amsterdam/3920654/ With SSLLabs: SSL Certificate: Signature Algorithm: ecdsa-with-SHA384 ECC Curve Name: secp384r1 ECC Key Strength: 192 Subject: current.openbsd.amsterdam Altnames: DNS:current.openbsd.amsterdam Issuer: E8 Also pointed a couple of "stress tests" against it and is holding well. Will leave it running for a while and report back when anything happens. Nice!! Mischa > > 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;