From: Rafael Sadowski Subject: Re: Relayd doesn't like ecdsa To: Mischa Cc: Tech Date: Mon, 15 Jun 2026 17:04:38 +0200 On Mon Jun 15, 2026 at 04:09:11PM +0200, Rafael Sadowski wrote: > On Wed Jun 03, 2026 at 12:45:27PM +0200, Mischa wrote: > > Hi Rafael, > > > > Not sure if this is relevant in this thread but... > > I got this message yesterday and the relayd process stopped. > > This is with the initial patch which Omar provided, running on 7.9. > > > > Jun 2 21:18:49 obsdams relayd[69181]: ecdsae_send_enc_imsg: priv ecdsa poll > > timeout, keyop #29f3 > > Jun 2 21:18:49 obsdams relayd[69181]: fatal in relay: proc_dispatch: relay > > 1 got invalid imsg 61 peerid -1 from ca 1 > > Jun 2 21:18:49 obsdams relayd[33605]: lost child: pid 69181 exited > > abnormally > > Jun 2 21:18:50 obsdams relayd[33597]: ecdsae_send_enc_imsg: imsgbuf_flush: > > Broken pipe > > > > Mischa > > > > I think you ran into the RELAY_TLS_PRIV_TIMEOUT (hard-coded 1sec) and > Omas's diff do not handle IMSG_CA_ECDSA_SIGN in relay_dispatch_ca > properly: > > diff --git a/relay.c b/relay.c > index 2f6ec5d..0c6dae0 100644 > --- a/relay.c > +++ b/relay.c > @@ -1980,6 +1980,7 @@ relay_dispatch_ca(int fd, struct privsep_proc *p, struct imsg *imsg) > switch (imsg_get_type(imsg)) { > case IMSG_CA_PRIVENC: > case IMSG_CA_PRIVDEC: > + case IMSG_CA_ECDSA_SIGN: > log_warnx("%s: priv%s result after timeout", __func__, > imsg_get_type(imsg) == IMSG_CA_PRIVENC ? "enc" : "dec"); > return (0); > Of course this was quick and dirty below is a better solution. Full diff if you want to test it. https://rsadowski.gothub.org/?action=summary&headref=feature/ecdsa&path=relayd.git diff --git a/ca.c b/ca.c index 6ce8626..0fb4877 100644 --- a/ca.c +++ b/ca.c @@ -223,9 +223,11 @@ ca_dispatch_relay(int fd, struct privsep_proc *p, struct imsg *imsg) struct ctl_keyop cko; EVP_PKEY *pkey; RSA *rsa; + EC_KEY *ecdsa; u_char *to = NULL; struct iovec iov[2]; - int c = 0; + int ret = 0, c = 0; + unsigned int len; switch (imsg_get_type(imsg)) { case IMSG_CA_PRIVENC: @@ -299,6 +301,64 @@ ca_dispatch_relay(int fd, struct privsep_proc *p, struct imsg *imsg) free(to); RSA_free(rsa); break; + + case IMSG_CA_ECDSA_SIGN: + if (imsg_get_ibuf(imsg, &ibuf) == -1) { + log_warn("%s: imsg_get_ibuf", __func__); + return (-1); + } + + if (ibuf_get(&ibuf, &cko, sizeof(cko)) == -1) { + log_warn("%s: ibuf_get", __func__); + return (-1); + } + + if (cko.cko_proc > env->sc_conf.prefork_relay) + fatalx("%s: invalid relay proc", __func__); + if (ibuf_size(&ibuf) != (size_t)cko.cko_flen) + fatalx("%s: invalid key operation", __func__); + + 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_get_type(imsg), -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("%s: calloc", __func__); + + ret = ECDSA_sign(0, ibuf_data(&ibuf), ibuf_size(&ibuf), to, + &len, ecdsa); + + cko.cko_tlen = (ret > 0) ? len : -1; + iov[c].iov_base = &cko; + iov[c++].iov_len = sizeof(cko); + if (ret > 0) { + iov[c].iov_base = to; + iov[c++].iov_len = len; + } + + if (proc_composev_imsg(env->sc_ps, PROC_RELAY, cko.cko_proc, + imsg_get_type(imsg), -1, -1, iov, c) == -1) + log_warn("%s: proc_composev_imsg", __func__); + + free(to); + EC_KEY_free(ecdsa); + break; + default: return -1; } @@ -389,7 +449,7 @@ rsae_send_imsg(int flen, const u_char *from, u_char *to, RSA *rsa, while (!done) { if ((n = imsgbuf_get(imsgbuf, &imsg)) == -1) - fatalx("imsg_get error"); + fatalx("imsgbuf_get error"); if (n == 0) break; @@ -455,14 +515,11 @@ rsae_priv_dec(int flen, const u_char *from, u_char *to, RSA *rsa, int padding) 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; @@ -494,3 +551,196 @@ ca_engine_init(struct relayd *x_env) ssl_error(errstr); 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 ibuf ibuf; + struct privsep *ps = env->sc_ps; + struct pollfd pfd[1]; + struct ctl_keyop cko; + struct iovec iov[2]; + struct imsgbuf *imsgbuf; + 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 (NULL); + + iev = proc_iev(ps, PROC_CA, ps->ps_instance); + imsgbuf = &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(imsgbuf, IMSG_CA_ECDSA_SIGN, 0, 0, -1, iov, cnt) == + -1) { + log_warn("%s: imsg_composev", __func__); + return (NULL); + } + if (imsgbuf_flush(imsgbuf) == -1) { + log_warn("%s: imsgbuf_flush", __func__); + return (NULL); + } + + pfd[0].fd = imsgbuf->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(imsgbuf)) == -1) + fatalx("imsgbuf_read"); + if (n == 0) + fatalx("pipe closed"); + + while (!done) { + if ((n = imsgbuf_get(imsgbuf, &imsg)) == -1) + fatalx("imsgbuf_get error"); + if (n == 0) + break; + + if (imsg_get_ibuf(&imsg, &ibuf) == -1) { + log_warn("%s: imsg_get_ibuf", __func__); + imsg_free(&imsg); + return (NULL); + } + + if (ibuf_get(&ibuf, &cko, sizeof(cko)) == -1) { + log_warn("%s: ibuf_get", __func__); + imsg_free(&imsg); + return (NULL); + } + + /* + * 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_get_type(&imsg) != 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 (ibuf_size(&ibuf) != (size_t)cko.cko_tlen) + fatalx("data size"); + toptr = ibuf_data(&ibuf); + d2i_ECDSA_SIG(&sig, &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(); +} diff --git a/relay.c b/relay.c index 2f6ec5d..8db6c32 100644 --- a/relay.c +++ b/relay.c @@ -1978,6 +1978,9 @@ int relay_dispatch_ca(int fd, struct privsep_proc *p, struct imsg *imsg) { switch (imsg_get_type(imsg)) { + case IMSG_CA_ECDSA_SIGN: + log_warnx("%s: priv result after timeout", __func__); + return (0); case IMSG_CA_PRIVENC: case IMSG_CA_PRIVDEC: log_warnx("%s: priv%s result after timeout", __func__, diff --git a/relayd.h b/relayd.h index c772300..dd666b6 100644 --- a/relayd.h +++ b/relayd.h @@ -1001,6 +1001,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 diff --git a/ssl.c b/ssl.c index 77f4dd1..c635648 100644 --- a/ssl.c +++ b/ssl.c @@ -189,6 +189,7 @@ ssl_load_pkey(char *buf, off_t len, X509 **x509ptr, EVP_PKEY **pkeyptr) 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) { @@ -204,21 +205,47 @@ ssl_load_pkey(char *buf, off_t len, X509 **x509ptr, EVP_PKEY **pkeyptr) 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 */ + /* dereference, will be cleaned up with pkey */ + RSA_free(rsa); + EC_KEY_free(eckey); *pkeyptr = pkey; if (x509ptr != NULL) *x509ptr = x509;