From: "Omar Polo" Subject: Re: Relayd doesn't like ecdsa To: Mischa Cc: Theo Buehler , Tech Date: Sat, 25 Apr 2026 19:10:42 +0200 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 =) 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;