root/usr.sbin/relayd/ca.c
/*      $OpenBSD: ca.c,v 1.50 2026/03/05 07:27:01 rsadowski Exp $       */

/*
 * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>
#include <sys/queue.h>
#include <sys/uio.h>

#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <imsg.h>

#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/x509.h>

#include "relayd.h"
#include "log.h"

void     ca_init(struct privsep *, struct privsep_proc *p, void *);
void     ca_launch(void);

int      ca_dispatch_parent(int, struct privsep_proc *, struct imsg *);
int      ca_dispatch_relay(int, struct privsep_proc *, struct imsg *);

int      rsae_priv_enc(int, const u_char *, u_char *, RSA *, int);
int      rsae_priv_dec(int, const u_char *, u_char *, RSA *, int);

static struct relayd *env = NULL;

static struct privsep_proc procs[] = {
        { "parent",     PROC_PARENT,    ca_dispatch_parent },
        { "relay",      PROC_RELAY,     ca_dispatch_relay },
};

void
ca(struct privsep *ps, struct privsep_proc *p)
{
        env = ps->ps_env;

        proc_run(ps, p, procs, nitems(procs), ca_init, NULL);
}

void
ca_init(struct privsep *ps, struct privsep_proc *p, void *arg)
{
        if (pledge("stdio recvfd", NULL) == -1)
                fatal("pledge");

        if (config_init(ps->ps_env) == -1)
                fatal("failed to initialize configuration");

        env->sc_id = getpid() & 0xffff;
}

void
hash_x509(X509 *cert, char *hash, size_t hashlen)
{
        static const char       hex[] = "0123456789abcdef";
        size_t                  off;
        char                    digest[EVP_MAX_MD_SIZE];
        int                     dlen, i;

        if (X509_pubkey_digest(cert, EVP_sha256(), digest, &dlen) != 1)
                fatalx("%s: X509_pubkey_digest failed", __func__);

        if (hashlen < 2 * dlen + sizeof("SHA256:"))
                fatalx("%s: hash buffer too small", __func__);

        off = strlcpy(hash, "SHA256:", hashlen);

        for (i = 0; i < dlen; i++) {
                hash[off++] = hex[(digest[i] >> 4) & 0x0f];
                hash[off++] = hex[digest[i] & 0x0f];
        }
        hash[off] = 0;
}

void
ca_launch(void)
{
        char                     hash[TLS_CERT_HASH_SIZE];
        char                    *buf;
        BIO                     *in = NULL;
        EVP_PKEY                *pkey = NULL;
        struct relay            *rlay;
        struct relay_cert       *cert;
        X509                    *x509 = NULL;
        off_t                    len;

        TAILQ_FOREACH(cert, env->sc_certs, cert_entry) {
                if (cert->cert_fd == -1 || cert->cert_key_fd == -1)
                        continue;

                if ((buf = relay_load_fd(cert->cert_fd, &len)) == NULL)
                        fatal("ca_launch: cert relay_load_fd");

                if ((in = BIO_new_mem_buf(buf, len)) == NULL)
                        fatalx("ca_launch: cert BIO_new_mem_buf");

                if ((x509 = PEM_read_bio_X509(in, NULL,
                    NULL, NULL)) == NULL)
                        fatalx("ca_launch: cert PEM_read_bio_X509");

                hash_x509(x509, hash, sizeof(hash));

                BIO_free(in);
                X509_free(x509);
                purge_key(&buf, len);

                if ((buf = relay_load_fd(cert->cert_key_fd, &len)) == NULL)
                        fatal("ca_launch: key relay_load_fd");

                if ((in = BIO_new_mem_buf(buf, len)) == NULL)
                        fatalx("%s: key", __func__);

                if ((pkey = PEM_read_bio_PrivateKey(in,
                    NULL, NULL, NULL)) == NULL)
                        fatalx("%s: PEM", __func__);

                cert->cert_pkey = pkey;

                if (pkey_add(env, pkey, hash) == NULL)
                        fatalx("tls pkey");

                BIO_free(in);
                purge_key(&buf, len);
        }

        TAILQ_FOREACH(rlay, env->sc_relays, rl_entry) {
                if ((rlay->rl_conf.flags & (F_TLS|F_TLSCLIENT)) == 0)
                        continue;

                if (rlay->rl_tls_cacert_fd != -1 &&
                    rlay->rl_conf.tls_cakey_len) {
                        if ((buf = relay_load_fd(rlay->rl_tls_cacert_fd,
                            &len)) == NULL)
                                fatal("ca_launch: cacert relay_load_fd");

                        if ((in = BIO_new_mem_buf(buf, len)) == NULL)
                                fatalx("ca_launch: cacert BIO_new_mem_buf");

                        if ((x509 = PEM_read_bio_X509(in, NULL,
                            NULL, NULL)) == NULL)
                                fatalx("ca_launch: cacert PEM_read_bio_X509");

                        hash_x509(x509, hash, sizeof(hash));

                        BIO_free(in);
                        X509_free(x509);
                        purge_key(&buf, len);

                        if ((in = BIO_new_mem_buf(rlay->rl_tls_cakey,
                            rlay->rl_conf.tls_cakey_len)) == NULL)
                                fatalx("%s: key", __func__);

                        if ((pkey = PEM_read_bio_PrivateKey(in,
                            NULL, NULL, NULL)) == NULL)
                                fatalx("%s: PEM", __func__);
                        BIO_free(in);

                        rlay->rl_tls_capkey = pkey;

                        if (pkey_add(env, pkey, hash) == NULL)
                                fatalx("ca pkey");

                        purge_key(&rlay->rl_tls_cakey,
                            rlay->rl_conf.tls_cakey_len);
                }
                close(rlay->rl_tls_ca_fd);
        }
}

int
ca_dispatch_parent(int fd, struct privsep_proc *p, struct imsg *imsg)
{
        switch (imsg->hdr.type) {
        case IMSG_CFG_RELAY:
                config_getrelay(env, imsg);
                break;
        case IMSG_CFG_RELAY_FD:
                config_getrelayfd(env, imsg);
                break;
        case IMSG_CFG_DONE:
                config_getcfg(env, imsg);
                break;
        case IMSG_CTL_START:
                ca_launch();
                break;
        case IMSG_CTL_RESET:
                config_getreset(env, imsg);
                break;
        default:
                return -1;
        }

        return 0;
}

int
ca_dispatch_relay(int fd, struct privsep_proc *p, struct imsg *imsg)
{
        struct ctl_keyop         cko;
        EVP_PKEY                *pkey;
        RSA                     *rsa;
        u_char                  *from = NULL, *to = NULL;
        struct iovec             iov[2];
        int                      c = 0;

        switch (imsg->hdr.type) {
        case IMSG_CA_PRIVENC:
        case IMSG_CA_PRIVDEC:
                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__);

                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 ((rsa = EVP_PKEY_get1_RSA(pkey)) == NULL)
                        fatalx("%s: invalid relay key", __func__);

                DPRINTF("%s:%d: key hash %s proc %d",
                    __func__, __LINE__, cko.cko_hash, cko.cko_proc);

                from = (u_char *)imsg->data + sizeof(cko);
                if ((to = calloc(1, cko.cko_tlen)) == NULL)
                        fatalx("%s: calloc", __func__);

                switch (imsg->hdr.type) {
                case IMSG_CA_PRIVENC:
                        cko.cko_tlen = RSA_private_encrypt(cko.cko_flen,
                            from, to, rsa, cko.cko_padding);
                        break;
                case IMSG_CA_PRIVDEC:
                        cko.cko_tlen = RSA_private_decrypt(cko.cko_flen,
                            from, to, rsa, cko.cko_padding);
                        break;
                }

                if (cko.cko_tlen == -1) {
                        char buf[256];
                        log_warnx("%s: %s", __func__,
                            ERR_error_string(ERR_get_error(), buf));
                }

                iov[c].iov_base = &cko;
                iov[c++].iov_len = sizeof(cko);
                if (cko.cko_tlen > 0) {
                        iov[c].iov_base = to;
                        iov[c++].iov_len = cko.cko_tlen;
                }

                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);
                RSA_free(rsa);
                break;
        default:
                return -1;
        }

        return 0;
}

/*
 * RSA privsep engine (called from unprivileged processes)
 */

static const RSA_METHOD *rsa_default;
static RSA_METHOD *rsae_method;

static int
rsae_send_imsg(int flen, const u_char *from, u_char *to, RSA *rsa,
    int padding, u_int cmd)
{
        struct privsep  *ps = env->sc_ps;
        struct pollfd    pfd[1];
        struct ctl_keyop cko;
        int              ret = 0;
        char            *hash;
        struct iovec     iov[2];
        struct imsgbuf  *ibuf;
        struct imsgev   *iev;
        struct imsg      imsg;
        int              n, done = 0, cnt = 0;
        u_char          *toptr;
        static u_int     seq = 0;

        if ((hash = RSA_get_ex_data(rsa, 0)) == NULL)
                return 0;

        iev = proc_iev(ps, PROC_CA, ps->ps_instance);
        ibuf = &iev->ibuf;

        /*
         * XXX this could be nicer...
         */

        (void)strlcpy(cko.cko_hash, hash, sizeof(cko.cko_hash));
        cko.cko_proc = ps->ps_instance;
        cko.cko_flen = flen;
        cko.cko_tlen = RSA_size(rsa);
        cko.cko_padding = padding;
        cko.cko_cookie = seq++;

        iov[cnt].iov_base = &cko;
        iov[cnt++].iov_len = sizeof(cko);
        iov[cnt].iov_base = (void *)(uintptr_t)from;
        iov[cnt++].iov_len = flen;

        /*
         * Send a synchronous imsg because we cannot defer the RSA
         * operation in OpenSSL.
         */
        if (imsg_composev(ibuf, cmd, 0, 0, -1, iov, cnt) == -1) {
                log_warn("%s: imsg_composev", __func__);
                return -1;
        }
        if (imsgbuf_flush(ibuf) == -1) {
                log_warn("%s: imsgbuf_flush", __func__);
                return -1;
        }

        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%s poll timeout, keyop #%x",
                            __func__,
                            cmd == IMSG_CA_PRIVENC ? "enc" : "dec",
                            cko.cko_cookie);
                        return -1;
                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%s obsolete keyop #%x", __func__,
                                    cmd == IMSG_CA_PRIVENC ? "enc" : "dec",
                                    cko.cko_cookie);
                                imsg_free(&imsg);
                                continue;
                        }

                        if (imsg.hdr.type != cmd)
                                fatalx("invalid response");

                        ret = cko.cko_tlen;
                        if (ret == -1) {
                                log_warnx("%s: priv%s failed for key %s",
                                    __func__, cmd == IMSG_CA_PRIVENC ?
                                    "enc" : "dec", cko.cko_hash);
                        } else if (ret > 0) {
                                if (IMSG_DATA_SIZE(&imsg) !=
                                    (sizeof(cko) + ret))
                                        fatalx("data size");
                                toptr = (u_char *)imsg.data + sizeof(cko);
                                memcpy(to, toptr, ret);
                        }
                        done = 1;

                        imsg_free(&imsg);
                }
        }
        imsg_event_add(iev);

        return ret;
}

int
rsae_priv_enc(int flen, const u_char *from, u_char *to, RSA *rsa, int padding)
{
        DPRINTF("%s:%d", __func__, __LINE__);
        return rsae_send_imsg(flen, from, to, rsa, padding, IMSG_CA_PRIVENC);
}

int
rsae_priv_dec(int flen, const u_char *from, u_char *to, RSA *rsa, int padding)
{
        DPRINTF("%s:%d", __func__, __LINE__);
        return rsae_send_imsg(flen, from, to, rsa, padding, IMSG_CA_PRIVDEC);
}

void
ca_engine_init(struct relayd *x_env)
{
        const char      *errstr;

        if (env == NULL)
                env = x_env;

        if (rsa_default != NULL)
                return;

        if ((rsa_default = RSA_get_default_method()) == NULL) {
                errstr = "RSA_get_default_method";
                goto fail;
        }

        if ((rsae_method = RSA_meth_dup(rsa_default)) == NULL) {
                errstr = "RSA_meth_dup";
                goto fail;
        }

        RSA_meth_set_priv_enc(rsae_method, rsae_priv_enc);
        RSA_meth_set_priv_dec(rsae_method, rsae_priv_dec);

        RSA_meth_set_flags(rsae_method,
            RSA_meth_get_flags(rsa_default) | RSA_METHOD_FLAG_NO_CHECK);
        RSA_meth_set0_app_data(rsae_method,
            RSA_meth_get0_app_data(rsa_default));

        RSA_set_default_method(rsae_method);

        return;

 fail:
        RSA_meth_free(rsae_method);
        fatalx("%s: %s", __func__, errstr);
}