root/usr.sbin/rpki-client/output.c
/*      $OpenBSD: output.c,v 1.45 2025/11/13 15:18:53 job Exp $ */
/*
 * Copyright (c) 2019 Theo de Raadt <deraadt@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.
 */

/*-
 * Copyright (C) 2009 Gabor Kovesdan <gabor@FreeBSD.org>
 * Copyright (C) 2012 Oleg Moskalenko <mom040267@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/stat.h>

#include <err.h>
#include <fcntl.h>
#include <unistd.h>
#include <netdb.h>
#include <signal.h>
#include <string.h>
#include <limits.h>
#include <time.h>

#include "extern.h"

int              outformats;

static char      output_tmpname[PATH_MAX];
static char      output_name[PATH_MAX];

static const struct outputs {
        int      format;
        char    *name;
        int     (*fn)(FILE *, struct validation_data *, struct stats *);
} outputs[] = {
        { FORMAT_OPENBGPD, "openbgpd", output_bgpd },
        { FORMAT_BIRD, "bird", output_bird },
        { FORMAT_CSV, "csv", output_csv },
        { FORMAT_JSON, "json", output_json },
        { FORMAT_OMETRIC, "metrics", output_ometric },
        { FORMAT_CCR, "rpki.ccr", output_ccr_der },
        { 0, NULL, NULL }
};

static FILE     *output_createtmp(char *);
static void      output_cleantmp(void);
static int       output_finish(FILE *, time_t);
static void      sig_handler(int);
static void      set_signal_handler(void);

/*
 * Detect & reject so-called "AS0 TALs".
 * AS0 TALs are TALs where for each and every subordinate ROA the asID field
 * set to 0. Such TALs introduce operational risk, as they change the fail-safe
 * from 'fail-open' to 'fail-closed'. Some context:
 *     https://lists.afrinic.net/pipermail/rpd/2021/013312.html
 *     https://lists.afrinic.net/pipermail/rpd/2021/013314.html
 */
static void
prune_as0_tals(struct vrp_tree *vrps)
{
        struct vrp *v, *tv;
        int talid;
        int has_vrps[TALSZ_MAX] = { 0 };
        int is_as0_tal[TALSZ_MAX] = { 0 };

        for (talid = 0; talid < talsz; talid++)
                is_as0_tal[talid] = 1;

        RB_FOREACH(v, vrp_tree, vrps) {
                has_vrps[v->talid] = 1;
                if (v->asid != 0)
                        is_as0_tal[v->talid] = 0;
        }

        for (talid = 0; talid < talsz; talid++) {
                if (is_as0_tal[talid] && has_vrps[talid]) {
                        warnx("%s: Detected AS0 TAL, pruning associated VRPs",
                            taldescs[talid]);
                }
        }

        RB_FOREACH_SAFE(v, vrp_tree, vrps, tv) {
                if (is_as0_tal[v->talid]) {
                        RB_REMOVE(vrp_tree, vrps, v);
                        free(v);
                }
        }

        /* XXX: update talstats? */
}

int
outputfiles(struct validation_data *vd, struct stats *st)
{
        int i, rc = 0;

        atexit(output_cleantmp);
        set_signal_handler();

        if (excludeas0)
                prune_as0_tals(&vd->vrps);

        for (i = 0; outputs[i].name; i++) {
                FILE *fout;

                if (!(outformats & outputs[i].format))
                        continue;

                fout = output_createtmp(outputs[i].name);
                if (fout == NULL) {
                        warn("cannot create %s", outputs[i].name);
                        rc = 1;
                        continue;
                }
                if ((*outputs[i].fn)(fout, vd, st) != 0) {
                        warn("output for %s format failed", outputs[i].name);
                        fclose(fout);
                        output_cleantmp();
                        rc = 1;
                        continue;
                }
                if (output_finish(fout, vd->buildtime) != 0) {
                        warn("finish for %s format failed", outputs[i].name);
                        output_cleantmp();
                        rc = 1;
                        continue;
                }
        }

        return rc;
}

static FILE *
output_createtmp(char *name)
{
        FILE *f;
        int fd, r;

        if (strlcpy(output_name, name, sizeof output_name) >=
            sizeof output_name)
                err(1, "path too long");
        r = snprintf(output_tmpname, sizeof output_tmpname,
            "%s.XXXXXXXXXXX", output_name);
        if (r < 0 || r > (int)sizeof(output_tmpname))
                err(1, "path too long");
        fd = mkostemp(output_tmpname, O_CLOEXEC);
        if (fd == -1)
                err(1, "mkostemp: %s", output_tmpname);
        (void) fchmod(fd, 0644);
        f = fdopen(fd, "w");
        if (f == NULL)
                err(1, "fdopen");
        return f;
}

static int
output_finish(FILE *out, time_t buildtime)
{
        struct timespec ts[2];

        if (fclose(out) != 0)
                return -1;

        ts[0].tv_nsec = UTIME_OMIT;
        ts[1].tv_sec = buildtime;
        ts[1].tv_nsec = 0;

        if (utimensat(AT_FDCWD, output_tmpname, ts, 0) == -1)
                return -1;

        if (rename(output_tmpname, output_name) == -1)
                return -1;

        output_tmpname[0] = '\0';
        return 0;
}

static void
output_cleantmp(void)
{
        if (*output_tmpname)
                unlink(output_tmpname);
        output_tmpname[0] = '\0';
}

/*
 * Signal handler that clears the temporary files.
 */
static void
sig_handler(int sig)
{
        output_cleantmp();
        _exit(2);
}

/*
 * Set signal handler on panic signals.
 */
static void
set_signal_handler(void)
{
        struct sigaction sa;
        int i, signals[] = {SIGTERM, SIGHUP, SIGINT, SIGUSR1, SIGUSR2,
            SIGPIPE, SIGXCPU, SIGXFSZ, 0};

        memset(&sa, 0, sizeof(sa));
        sigfillset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        sa.sa_handler = sig_handler;

        for (i = 0; signals[i] != 0; i++) {
                if (sigaction(signals[i], &sa, NULL) == -1) {
                        warn("sigaction(%s)", strsignal(signals[i]));
                        continue;
                }
        }
}

int
outputheader(FILE *out, struct validation_data *vd, struct stats *st)
{
        char            hn[NI_MAXHOST], tbuf[80];
        struct tm       *tp;
        int             i;

        tp = gmtime(&vd->buildtime);
        strftime(tbuf, sizeof tbuf, "%a %b %e %H:%M:%S UTC %Y", tp);

        gethostname(hn, sizeof hn);

        if (fprintf(out,
            "# Generated on host %s at %s\n"
            "# Processing time %lld seconds (%llds user, %llds system)\n"
            "# CCR manifest hash: %s\n"
            "# CCR validated ROA payloads hash: %s\n"
            "# CCR validated ASPA payloads hash: %s\n"
            "# Route Origin Authorizations: %u (%u failed parse, %u invalid)\n"
            "# BGPsec Router Certificates: %u\n"
            "# Certificates: %u (%u invalid, %u non-functional)\n",
            hn, tbuf, (long long)st->elapsed_time.tv_sec,
            (long long)st->user_time.tv_sec, (long long)st->system_time.tv_sec,
            vd->ccr.mfts_hash, vd->ccr.vrps_hash, vd->ccr.vaps_hash,
            st->repo_tal_stats.roas, st->repo_tal_stats.roas_fail,
            st->repo_tal_stats.roas_invalid, st->repo_tal_stats.brks,
            st->repo_tal_stats.certs, st->repo_tal_stats.certs_fail,
            st->repo_tal_stats.certs_nonfunc) < 0)
                return -1;

        if (fprintf(out,
            "# Trust Anchor Locators: %u (%u invalid) [", st->tals,
            talsz - st->tals) < 0)
                return -1;
        for (i = 0; i < talsz; i++)
                if (fprintf(out, " %s", tals[i]) < 0)
                        return -1;

        if (fprintf(out,
            " ]\n"
            "# Manifests: %u (%u failed parse)\n"
            "# Certificate revocation lists: %u\n"
            "# Repositories: %u\n"
            "# VRP Entries: %u (%u unique)\n",
            st->repo_tal_stats.mfts, st->repo_tal_stats.mfts_fail,
            st->repo_tal_stats.crls,
            st->repos,
            st->repo_tal_stats.vrps, st->repo_tal_stats.vrps_uniqs) < 0)
                return -1;
        return 0;
}