From: Job Snijders Subject: rpki-client: give priority to confirmed certification paths To: tech@openbsd.org Date: Mon, 5 Oct 2026 10:47:53 +0000 Dear all, Because SKI might not be globally unique, certificates are tracked by an internal identifier since 2024. Duplicate SKI might appear because of reuse of a keypair (a poor CA practise, but not illegal), or because of 'public key stealing', i.e., an adversarial CA issues a certificate with the victim CA's SPKI and SIA. RPs cannot easily distinguish the causes of duplicate SKIs, and there is no proof of possession in the RPKI. Jacobsen et al., 2026, suggested that rpki-client's certification path loop protection might be exploited if an adversary can race the victim. This race can happen in the discovery of the logical certification topology, unrelated to speedrunning repository loading. Any payload carrying objects (ROAs, ASPAs, etc) discovered through the adversary's certification path will fail resource containment checks and the loop guard (entity_process()->filepath_add()) necessarily enforces each object to be accessed only once. The result is that the victim's payloads disappear from view if they lose the race. The Jacobsen paper incorrectly asserts that RPKI certificates cannot be pinned to a logical location. While indeed AIAs are merely of an advisory quality, CRLDPs are not. RFC 9829 clarifies that a resource certificate cannot be validated without consulting the current manifest of the certificate's issuer, RFC 9981 section 4 specifies anchoring of manifests, and RFC 6480, section 4.2, illustrates how all valid products in the same CA repository (i.e., from the same issuer, listed on the same manifest) will point to the same CRL. CRLs and CA certificates are referenced from manifests by SHA-256. In short: a standards-compliant RP will verify CA/EE CRLDP and manifest SIA alignment and thus can robustly construct a concept of placement of certificates in the logical topology. Based on the above, an effective mitigation against certification path poisoning is to keep track of all the certificate->manifest segments that yielded validated payloads in the past and prioritise discovery through those 'confirmed' segments in the future. CA certificates become 'confirmed' once a valid non-inheriting end-entity certificate is found. I think this mitigation will work well in practise because potential adversaries and victims won't have authority for the same resources. The listing of confirmed CAs is stored in the cache directory for use in the next run, somewhat similar to what's done for non-functional CAs. Each entry contains a AKI, SKI, certificate location (this is not an AIA), the manifest location, and confirmation timestamp. Unconfirmed CAs are deprioritised by adding the respective manifest entity to a tail queue which is processed only after all confirmed CAs are processed. Through SIGINFO one can nicely see the two queue approach in action. Statistics are collected on the number of unconfirmed CAs per repo & tal. These new stats shows that at the moment of writing a few thousand RPKI CAs do not publish any ROAs, ASPAs, SPLs, or BGPsec router keys, and therefore could not (yet) be confirmed. This is as expected. OK? Kind regards, Job Index: Makefile =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/Makefile,v diff -u -p -r1.43 Makefile --- Makefile 4 Oct 2026 09:39:02 -0000 1.43 +++ Makefile 5 Oct 2026 08:13:26 -0000 @@ -8,6 +8,7 @@ SRCS+= aspa.c SRCS+= bs_ber.c SRCS+= bs_cbb.c SRCS+= bs_cbs.c +SRCS+= cca.c SRCS+= ccr.c SRCS+= cert.c SRCS+= cms.c Index: cca.c =================================================================== RCS file: cca.c diff -N cca.c --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ cca.c 5 Oct 2026 08:13:26 -0000 @@ -0,0 +1,450 @@ +/* $OpenBSD$ */ +/* + * Copyright (c) 2026 Job Snijders + * + * 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "extern.h" + +/* + * Structure to hold identifier details used to confirm segments of + * certification paths. A cert-to-mft segment becomes 'confirmed' once a + * valid non-inheriting end-entity certificate is found through that path. + */ +struct cca { + RB_ENTRY(cca) entry; + char *aki; + char *ski; + char *location; + char *mfturi; + enum cert_purpose purpose; + int certid; + int issuerid; + int talid; + unsigned int repoid; + time_t since; + int confirmed; +}; + +static RB_HEAD(cca_tree, cca) ccas = RB_INITIALIZER(&ccas); + +static inline int +certid_cmp(const struct cca *a, const struct cca *b) +{ + if (a->certid < b->certid) + return -1; + if (a->certid > b->certid) + return 1; + + return 0; +} + +RB_GENERATE_STATIC(cca_tree, cca, entry, certid_cmp); + +static RB_HEAD(prev_cca_tree, cca) prev_ccas = RB_INITIALIZER(&prev_ccas); + +static inline int +prev_cca_cmp(const struct cca *a, const struct cca *b) +{ + int cmp; + + cmp = strcmp(a->ski, b->ski); + if (cmp > 0) + return 1; + if (cmp < 0) + return -1; + + cmp = strcmp(a->aki, b->aki); + if (cmp > 0) + return 1; + if (cmp < 0) + return -1; + + cmp = strcmp(a->location, b->location); + if (cmp > 0) + return 1; + if (cmp < 0) + return -1; + + cmp = strcmp(a->mfturi, b->mfturi); + if (cmp > 0) + return 1; + if (cmp < 0) + return -1; + + return 0; +} + +RB_GENERATE_STATIC(prev_cca_tree, cca, entry, prev_cca_cmp); + +static time_t +cca_lookup_since(const struct cert *cert) +{ + struct cca *found, needle; + time_t since = 0; + + needle.aki = cert->aki; + needle.ski = cert->ski; + needle.location = cert->path; + needle.mfturi = cert->mft; + + if ((found = RB_FIND(prev_cca_tree, &prev_ccas, &needle)) != NULL) + since = found->since; + + return since; +} + +static void +cca_free(struct cca *cca) +{ + free(cca->aki); + free(cca->ski); + free(cca->location); + free(cca->mfturi); + free(cca); +} + +/* + * Add identifiers from the given cert into the tree of to-be-confirmed CAs. + */ +void +cca_tree_insert(const struct cert *cert) +{ + struct cca *cca; + + assert(cert->purpose == CERT_PURPOSE_TA || + cert->purpose == CERT_PURPOSE_CA); + + if ((cca = calloc(1, sizeof(*cca))) == NULL) + err(1, NULL); + + if (cert->purpose == CERT_PURPOSE_CA) { + if ((cca->aki = strdup(cert->aki)) == NULL) + err(1, NULL); + } + + if ((cca->ski = strdup(cert->ski)) == NULL) + err(1, NULL); + if ((cca->location = strdup(cert->path)) == NULL) + err(1, NULL); + if ((cca->mfturi = strdup(cert->mft)) == NULL) + err(1, NULL); + + cca->certid = cert->certid; + cca->issuerid = cert->issuerid; + cca->talid = cert->talid; + cca->repoid = cert->repoid; + cca->purpose = cert->purpose; + cca->since = cca_lookup_since(cert); + cca->confirmed = 0; + + if (RB_INSERT(cca_tree, &ccas, cca) != NULL) { + warnx("duplicate CA in cca_tree at %s", cca->location); + cca_free(cca); + } +} + +/* + * Check whether a CA was previously confirmed. + * Returns 1 if confirmed, 0 otherwise. + */ +int +cca_was_confirmed(int cid) +{ + struct cca *found, needle = { .certid = cid }; + + if ((found = RB_FIND(cca_tree, &ccas, &needle)) != NULL) { + if (found->confirmed || found->since != 0) + return 1; + } + + return 0; +} + +/* + * Mark the given CA and its parents as confirmed. + * Return 1 if the state changed, 0 otherwise. + */ +void +cca_confirm_ca(int cid) +{ + struct cca *found, needle = { .certid = cid }; + time_t now = get_current_time(); + + found = RB_FIND(cca_tree, &ccas, &needle); + assert(found != NULL); + + if (!found->confirmed && found->purpose == CERT_PURPOSE_CA) + cca_confirm_ca(found->issuerid); + + if (found->since == 0) + found->since = now; + + found->confirmed = 1; +} + +static void +prev_cca_tree_free(void) +{ + struct cca *cca, *cca_tmp; + + RB_FOREACH_SAFE(cca, prev_cca_tree, &prev_ccas, cca_tmp) { + RB_REMOVE(prev_cca_tree, &prev_ccas, cca); + cca_free(cca); + } +} + +void +cca_load(void) +{ + FILE *f; + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + const char *errstr; + struct cca *cca = NULL; + time_t now = get_current_time(); + + if ((f = fopen(CONFIRMED_CA, "r")) == NULL) { + if (errno == ENOENT) + return; + err(1, "failed to open %s", CONFIRMED_CA); + } + + while ((linelen = getline(&line, &linesize, f)) != -1) { + char *l, *aki, *ski, *since, *loc, *mfturi; + size_t loc_len, mfturi_len; + + if (line[linelen - 1] == '\n') + line[linelen - 1] = '\0'; + + if ((cca = calloc(1, sizeof(*cca))) == NULL) + err(1, NULL); + + l = line; + + if ((aki = strsep(&l, " ")) == NULL) + goto err; + if ((cca->aki = strdup(aki)) == NULL) + err(1, NULL); + + if ((ski = strsep(&l, " ")) == NULL) + goto err; + if ((cca->ski = strdup(ski)) == NULL) + err(1, NULL); + + if ((since = strsep(&l, " ")) == NULL) + goto err; + cca->since = strtonum(since, 1, LLONG_MAX, &errstr); + if (errstr != NULL) + goto err; + if (cca->since > now) + goto err; + + if ((loc = strsep(&l, " ")) == NULL) + goto err; + + /* minimal example cert location: ab.cd/a/b.cer */ + if ((loc_len = strlen(loc)) < 13) + goto err; + if (strcmp(loc + loc_len - 4, ".cer") != 0) + goto err; + if (!valid_uri(loc, strlen(loc), NULL)) + goto err; + if ((cca->location = strdup(loc)) == NULL) + err(1, NULL); + + mfturi = l; + if (mfturi == NULL) + goto err; + + /* minimal example mft location: rsync://a.bc/d/e.mft */ + if ((mfturi_len = strlen(mfturi)) < 20) + goto err; + if (strcmp(mfturi + mfturi_len - 4, ".mft") != 0) + goto err; + if (!valid_uri(mfturi, strlen(mfturi), RSYNC_PROTO)) + goto err; + if ((cca->mfturi = strdup(mfturi)) == NULL) + err(1, NULL); + + if (RB_INSERT(prev_cca_tree, &prev_ccas, cca) != NULL) { + warnx("duplicate entry for cca_tree at %s", + cca->location); + cca_free(cca); + cca = NULL; + } + } + + if (ferror(f)) + goto err; + + fclose(f); + free(line); + + return; + + err: + warnx("error reading %s", CONFIRMED_CA); + fclose(f); + unlink(CONFIRMED_CA); + + free(line); + + cca_free(cca); + + prev_cca_tree_free(); +} + +static int +ccas_sorted_cmp(const void *a, const void *b) +{ + int cmp; + const struct cca *cca_a = *(const struct cca **)a; + const struct cca *cca_b = *(const struct cca **)b; + + cmp = strcmp(cca_a->location, cca_b->location); + if (cmp > 0) + return 1; + if (cmp < 0) + return -1; + + cmp = strcmp(cca_a->mfturi, cca_b->mfturi); + if (cmp > 0) + return 1; + if (cmp < 0) + return -1; + + cmp = strcmp(cca_a->aki, cca_b->aki); + if (cmp > 0) + return 1; + if (cmp < 0) + return -1; + + cmp = strcmp(cca_a->ski, cca_b->ski); + if (cmp > 0) + return 1; + if (cmp < 0) + return -1; + + return 0; +} + +static void +cca_tree_free(void) +{ + struct cca *cca, *cca_tmp; + + RB_FOREACH_SAFE(cca, cca_tree, &ccas, cca_tmp) { + RB_REMOVE(cca_tree, &ccas, cca); + cca_free(cca); + } +} + +void +cca_save(time_t buildtime) +{ + char temp[] = CONFIRMED_CA ".XXXXXXXX"; + FILE *f = NULL; + int fd; + struct cca *cca, *cca_tmp, **ccas_sorted = NULL; + size_t ccas_num = 0, idx = 0; + struct timespec ts[2]; + + prev_cca_tree_free(); + + if (RB_EMPTY(&ccas)) { + unlink(CONFIRMED_CA); + return; + } + + if ((fd = mkostemp(temp, O_CLOEXEC)) == -1) + goto err; + (void)fchmod(fd, 0644); + + if ((f = fdopen(fd, "w")) == NULL) + err(1, "fopen"); + + RB_FOREACH_SAFE(cca, cca_tree, &ccas, cca_tmp) { + if (cca->confirmed && cca->purpose == CERT_PURPOSE_CA) + ccas_num++; + else { + RB_REMOVE(cca_tree, &ccas, cca); + cca_free(cca); + cca = NULL; + } + } + + if ((ccas_sorted = calloc(ccas_num, sizeof(ccas_sorted[0]))) == NULL) + err(1, NULL); + + RB_FOREACH(cca, cca_tree, &ccas) { + repo_stat_dec_cca(cca->repoid, cca->talid); + ccas_sorted[idx++] = cca; + } + + qsort(ccas_sorted, ccas_num, sizeof(ccas_sorted[0]), ccas_sorted_cmp); + + for (idx = 0; idx < ccas_num; idx++) { + cca = ccas_sorted[idx]; + + if (fprintf(f, "%s %s %lld %s %s\n", cca->aki, cca->ski, + (long long)cca->since, cca->location, cca->mfturi) < 0) + goto err; + } + + if (fclose(f) != 0) { + f = NULL; + goto err; + } + f = NULL; + + ts[0].tv_nsec = UTIME_OMIT; + ts[1].tv_sec = buildtime; + ts[1].tv_nsec = 0; + + if (utimensat(AT_FDCWD, temp, ts, 0) == -1) + goto err; + + if (rename(temp, CONFIRMED_CA) == -1) + goto err; + + free(ccas_sorted); + cca_tree_free(); + + return; + + err: + warn("error saving confirmed CAs to %s", temp); + if (f != NULL) + fclose(f); + unlink(temp); + free(ccas_sorted); + cca_tree_free(); +} Index: extern.h =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/extern.h,v diff -u -p -r1.300 extern.h --- extern.h 29 Sep 2026 21:59:14 -0000 1.300 +++ extern.h 5 Oct 2026 08:13:26 -0000 @@ -138,6 +138,7 @@ struct cert { size_t num_ases; int talid; /* cert is covered by which TAL */ int certid; + int issuerid; /* id of the issuer of this cert */ unsigned int repoid; /* repository of this cert file */ char *path; /* filename without .rrdp and .rsync prefix */ char *repo; /* CA repository (rsync:// uri) */ @@ -314,6 +315,7 @@ struct roa { uint32_t asid; /* asID of ROA (if 0, RFC 6483 sec 4) */ struct roa_ip *ips; /* IP prefixes */ size_t num_ips; + int issuerid; /* certid of the issuer */ int talid; /* ROAs are covered by which TAL */ int valid; /* validated resources */ time_t signtime; /* CMS signing-time attribute */ @@ -357,6 +359,7 @@ struct spl { uint32_t asid; struct spl_pfx *prefixes; size_t num_prefixes; + int issuerid; /* certid of the issuer */ int talid; time_t signtime; /* CMS signing-time attribute */ time_t expires; /* when the certification path expires */ @@ -393,6 +396,7 @@ struct tak { */ struct aspa { int valid; /* contained in issuer auth */ + int issuerid; /* certid of the issuer */ int talid; /* TAL the ASPA is chained up to */ uint32_t custasid; /* the customerASID */ uint32_t *providers; /* the providers */ @@ -648,6 +652,7 @@ enum stype { STYPE_PROVIDERS, STYPE_OVERFLOW, STYPE_SEQNUM_GAP, + STYPE_UNCONFIRMED, }; struct repo; @@ -663,6 +668,7 @@ struct repotalstats { uint32_t certs_fail; /* invalid certificate */ uint32_t certs_nonfunc; /* non-functional CA certificates */ uint32_t certs_nonfunc_deferred; + uint32_t certs_unconfirmed; uint32_t mfts; /* total number of manifests */ uint32_t mfts_gap; /* manifests with sequence gaps */ uint32_t mfts_fail; /* failing syntactic parse */ @@ -773,6 +779,12 @@ struct cert *ta_validate(const char *, s struct cert *cert_read(struct ibuf *); void cert_insert_brks(struct brk_tree *, struct cert *); +void cca_load(void); +void cca_save(time_t); +void cca_tree_insert(const struct cert *); +void cca_confirm_ca(int); +int cca_was_confirmed(int); + void nca_history_load(void); void nca_history_save(struct nca_tree *, time_t); int nca_skip_sync(struct nca_tree *, const struct cert *); @@ -931,12 +943,13 @@ struct repo *ta_lookup(int, struct tal * struct repo *repo_lookup(int, const char *, const char *); struct repo *repo_byid(unsigned int); int repo_queued(struct repo *, struct entity *); -void repo_printinfo(size_t); +void repo_printinfo(size_t, size_t); void repo_cleanup(struct filepath_tree *, int); int repo_check_timeout(int); void repostats_new_files_inc(struct repo *, const char *); void repo_stat_inc(struct repo *, int, enum rtype, enum stype); void repo_stat_add_nca(struct nonfunc_ca *); +void repo_stat_dec_cca(unsigned int, int); void repo_tal_stats_collect(void (*)(const struct repo *, const struct repotalstats *, void *), int, void *); void repo_stats_collect(void (*)(const struct repo *, @@ -1135,6 +1148,7 @@ int mkpathat(int, const char *); #define RSYNC_PROTO "rsync://" #define RSYNC_PROTO_LEN (sizeof(RSYNC_PROTO) - 1) +#define CONFIRMED_CA ".confirmed_ca" #define NCA_HISTORY ".nca_history" /* Compat helpers for OpenSSL < 4 and LibreSSL. */ Index: main.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/main.c,v diff -u -p -r1.316 main.c --- main.c 30 Sep 2026 20:48:11 -0000 1.316 +++ main.c 5 Oct 2026 08:13:27 -0000 @@ -54,7 +54,7 @@ unsigned int talrepocnt[TALSZ_MAX]; struct repotalstats talstats[TALSZ_MAX]; int talsz; -size_t entity_queue; +size_t entity_queue, entity_lowprio_queue; int timeout = 60*60; volatile sig_atomic_t killme; volatile sig_atomic_t printinfo; @@ -64,6 +64,8 @@ static struct filepath_tree fpt = RB_INI static struct msgbuf *procq, *rsyncq, *httpq, *rrdpq; static int cachefd, outdirfd; +static struct entityq lowprio_queue = TAILQ_HEAD_INITIALIZER(lowprio_queue); + int verbose; int noop; int excludeas0 = 1; @@ -288,6 +290,17 @@ entityq_add(char *path, char *file, enum p->data = data; p->datasz = (data != NULL) ? datasz : 0; + /* + * Deprioritise and delay processing of manifest pointers from CAs which + * haven't (yet) been confirmed to be part of valid certification + * chains towards non-inheriting EE certs. + */ + if (type == RTYPE_MFT && !cca_was_confirmed(certid)) { + TAILQ_INSERT_TAIL(&lowprio_queue, p, entries); + entity_lowprio_queue++; + return; + } + entity_queue++; /* @@ -663,14 +676,19 @@ entity_process(struct ibuf *b, struct va cert = cert_read(b); switch (cert->purpose) { case CERT_PURPOSE_TA: + cca_tree_insert(cert); + cca_confirm_ca(cert->certid); queue_add_from_cert(cert, &vd->ncas); ccr_insert_tas(&vd->ccr.tas, cert); break; case CERT_PURPOSE_CA: + cca_tree_insert(cert); + repo_stat_inc(rp, talid, type, STYPE_UNCONFIRMED); queue_add_from_cert(cert, &vd->ncas); ccr_insert_mft_sub(&vd->ccr.mfts, cert); break; case CERT_PURPOSE_BGPSEC_ROUTER: + cca_confirm_ca(cert->issuerid); cert_insert_brks(&vd->brks, cert); repo_stat_inc(rp, talid, type, STYPE_BGPSEC); break; @@ -706,6 +724,7 @@ entity_process(struct ibuf *b, struct va if (roa->valid) { roa_insert_vrps(&vd->vrps, roa, rp); ccr_insert_roa(&vd->ccr.vrps, roa); + cca_confirm_ca(roa->issuerid); } else repo_stat_inc(rp, talid, type, STYPE_INVALID); roa_free(roa); @@ -717,9 +736,10 @@ entity_process(struct ibuf *b, struct va break; } aspa = aspa_read(b); - if (aspa->valid) + if (aspa->valid) { aspa_insert_vaps(file, &vd->vaps, aspa, rp); - else + cca_confirm_ca(aspa->issuerid); + } else repo_stat_inc(rp, talid, type, STYPE_INVALID); aspa_free(aspa); break; @@ -731,9 +751,10 @@ entity_process(struct ibuf *b, struct va break; } spl = spl_read(b); - if (spl->valid) + if (spl->valid) { spl_insert_vsps(&vd->vsps, spl, rp); - else + cca_confirm_ca(spl->issuerid); + } else repo_stat_inc(rp, talid, type, STYPE_INVALID); spl_free(spl); break; @@ -826,6 +847,7 @@ sum_stats(const struct repo *rp, const s out->certs_fail += in->certs_fail; out->certs_nonfunc += in->certs_nonfunc; out->certs_nonfunc_deferred += in->certs_nonfunc_deferred; + out->certs_unconfirmed += in->certs_unconfirmed; out->roas += in->roas; out->roas_fail += in->roas_fail; out->roas_invalid += in->roas_invalid; @@ -1364,14 +1386,36 @@ main(int argc, char *argv[]) if (fchdir(cachefd) == -1) err(1, "fchdir"); + cca_load(); + nca_history_load(); - while (entity_queue > 0 && !killme) { + while ((entity_queue > 0 || entity_lowprio_queue > 0) && !killme) { int polltim; if (printinfo) { printinfo = 0; - repo_printinfo(entity_queue); + repo_printinfo(entity_queue, entity_lowprio_queue); + } + + /* + * If there isn't much else to do, work the low priority queue. + */ + if (entity_queue == 0 && entity_lowprio_queue > 0) { + struct entity *e, *e_tmp; + struct repo *rp; + + TAILQ_FOREACH_SAFE(e, &lowprio_queue, entries, e_tmp) { + TAILQ_REMOVE(&lowprio_queue, e, entries); + entity_lowprio_queue--; + + entity_queue++; + rp = repo_byid(e->repoid); + if (!repo_queued(rp, e)) { + entity_write_req(e); + entity_free(e); + } + } } polltim = repo_check_timeout(INFTIM); @@ -1493,7 +1537,7 @@ main(int argc, char *argv[]) signal(SIGALRM, SIG_DFL); if (killme) { - repo_printinfo(entity_queue); + repo_printinfo(entity_queue, entity_lowprio_queue); syslog(LOG_CRIT|LOG_DAEMON, "excessive runtime (%d seconds), giving up", timeout); errx(1, "excessive runtime (%d seconds), giving up", timeout); @@ -1549,7 +1593,7 @@ main(int argc, char *argv[]) /* processing did not finish because of error */ if (entity_queue != 0) { - repo_printinfo(entity_queue); + repo_printinfo(entity_queue, entity_lowprio_queue); errx(1, "not all files processed, giving up"); } @@ -1582,8 +1626,10 @@ main(int argc, char *argv[]) vd.buildtime = get_current_time(); - if (!noop && rc == 0) + if (!noop && rc == 0) { + cca_save(vd.buildtime); nca_history_save(&vd.ncas, vd.buildtime); + } /* change working directory to the output directory */ if (fchdir(outdirfd) == -1) @@ -1625,10 +1671,11 @@ main(int argc, char *argv[]) } printf("BGPsec Router Certificates: %u\n", stats.repo_tal_stats.brks); printf("Certificates: %u (%u invalid, %u non-functional, %u sync " - "deferred)\n", stats.repo_tal_stats.certs, + "deferred, %u unconfirmed)\n", stats.repo_tal_stats.certs, stats.repo_tal_stats.certs_fail, stats.repo_tal_stats.certs_nonfunc, - stats.repo_tal_stats.certs_nonfunc_deferred); + stats.repo_tal_stats.certs_nonfunc_deferred, + stats.repo_tal_stats.certs_unconfirmed); printf("Trust Anchor Locators: %u (%u invalid)\n", stats.tals, talsz - stats.tals); printf("Manifests: %u (%u failed parse, %u seqnum gaps)\n", Index: parser.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/parser.c,v diff -u -p -r1.189 parser.c --- parser.c 24 Sep 2026 14:44:03 -0000 1.189 +++ parser.c 5 Oct 2026 08:13:27 -0000 @@ -225,6 +225,7 @@ proc_parser_roa(char *file, const unsign goto out; } + roa->issuerid = entp->certid; roa->talid = a->cert->talid; roa->expires = x509_find_expires(cert->notafter, a, &crls); @@ -267,6 +268,7 @@ proc_parser_spl(char *file, const unsign goto out; } + spl->issuerid = entp->certid; spl->talid = a->cert->talid; spl->expires = x509_find_expires(cert->notafter, a, &crls); @@ -631,6 +633,10 @@ proc_parser_cert(char *file, const unsig goto out; } + if (cert->purpose == CERT_PURPOSE_TA) + cert->issuerid = a->cert->talid; + else + cert->issuerid = a->cert->certid; cert->talid = a->cert->talid; cert->path = parse_filepath(entp->repoid, entp->path, entp->file, @@ -791,6 +797,7 @@ proc_parser_aspa(char *file, const unsig goto out; } + aspa->issuerid = entp->certid; aspa->talid = a->cert->talid; aspa->expires = x509_find_expires(cert->notafter, a, &crls); Index: repo.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/repo.c,v diff -u -p -r1.92 repo.c --- repo.c 1 Oct 2026 13:06:56 -0000 1.92 +++ repo.c 5 Oct 2026 08:13:27 -0000 @@ -1442,11 +1442,12 @@ repo_queued(struct repo *rp, struct enti } void -repo_printinfo(size_t qlen) +repo_printinfo(size_t qlen, size_t lpqlen) { struct repo *rp; - warnx("%zu outstanding entities", qlen); + warnx("Outstanding entities: %zu regular, %zu lower priority", qlen, + lpqlen); SLIST_FOREACH(rp, &repos, entry) { if (TAILQ_EMPTY(&rp->queue)) @@ -1563,6 +1564,19 @@ repo_stat_add_nca(struct nonfunc_ca *nca } } +void +repo_stat_dec_cca(unsigned int rid, int tid) +{ + struct repo *rp; + + SLIST_FOREACH(rp, &repos, entry) { + if (rp->id == rid) { + rp->stats[tid].certs_unconfirmed--; + break; + } + } +} + /* * Update stats object of repository depending on rtype and subtype. */ @@ -1578,6 +1592,8 @@ repo_stat_inc(struct repo *rp, int talid rp->stats[talid].certs++; if (subtype == STYPE_FAIL) rp->stats[talid].certs_fail++; + if (subtype == STYPE_UNCONFIRMED) + rp->stats[talid].certs_unconfirmed++; if (subtype == STYPE_BGPSEC) { rp->stats[talid].certs--; rp->stats[talid].brks++; @@ -1880,9 +1896,12 @@ repo_cleanup_entry(FTSENT *e, struct fil path = skip_dotslash(e->fts_path); switch (e->fts_info) { case FTS_NSOK: - if (e->fts_level == 1 && fts_state.type == BASE_DIR && - strcmp(e->fts_name, NCA_HISTORY) == 0) - break; + if (e->fts_level == 1) { + if (strcmp(e->fts_name, CONFIRMED_CA) == 0) + break; + if (strcmp(e->fts_name, NCA_HISTORY) == 0) + break; + } if (filepath_exists(tree, path)) { e->fts_parent->fts_number++; break; Index: cert.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/cert.c,v diff -u -p -r1.244 cert.c --- cert.c 13 Jul 2026 10:53:12 -0000 1.244 +++ cert.c 5 Oct 2026 08:13:28 -0000 @@ -1823,6 +1823,7 @@ cert_buffer(struct ibuf *b, const struct io_simple_buffer(b, &cert->notafter, sizeof(cert->notafter)); io_simple_buffer(b, &cert->purpose, sizeof(cert->purpose)); io_simple_buffer(b, &cert->talid, sizeof(cert->talid)); + io_simple_buffer(b, &cert->issuerid, sizeof(cert->issuerid)); io_simple_buffer(b, &cert->certid, sizeof(cert->certid)); io_simple_buffer(b, &cert->repoid, sizeof(cert->repoid)); io_simple_buffer(b, &cert->num_ips, sizeof(cert->num_ips)); @@ -1878,6 +1879,7 @@ cert_read(struct ibuf *b) io_read_buf(b, &cert->notafter, sizeof(cert->notafter)); io_read_buf(b, &cert->purpose, sizeof(cert->purpose)); io_read_buf(b, &cert->talid, sizeof(cert->talid)); + io_read_buf(b, &cert->issuerid, sizeof(cert->issuerid)); io_read_buf(b, &cert->certid, sizeof(cert->certid)); io_read_buf(b, &cert->repoid, sizeof(cert->repoid)); io_read_buf(b, &cert->num_ips, sizeof(cert->num_ips)); Index: aspa.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/aspa.c,v diff -u -p -r1.47 aspa.c --- aspa.c 14 Sep 2026 09:21:41 -0000 1.47 +++ aspa.c 5 Oct 2026 08:13:28 -0000 @@ -244,6 +244,7 @@ aspa_buffer(struct ibuf *b, const struct { io_simple_buffer(b, &aspa->valid, sizeof(aspa->valid)); io_simple_buffer(b, &aspa->custasid, sizeof(aspa->custasid)); + io_simple_buffer(b, &aspa->issuerid, sizeof(aspa->issuerid)); io_simple_buffer(b, &aspa->talid, sizeof(aspa->talid)); io_simple_buffer(b, &aspa->expires, sizeof(aspa->expires)); @@ -267,6 +268,7 @@ aspa_read(struct ibuf *b) io_read_buf(b, &aspa->valid, sizeof(aspa->valid)); io_read_buf(b, &aspa->custasid, sizeof(aspa->custasid)); + io_read_buf(b, &aspa->issuerid, sizeof(aspa->issuerid)); io_read_buf(b, &aspa->talid, sizeof(aspa->talid)); io_read_buf(b, &aspa->expires, sizeof(aspa->expires)); Index: roa.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/roa.c,v diff -u -p -r1.93 roa.c --- roa.c 14 Sep 2026 09:21:41 -0000 1.93 +++ roa.c 5 Oct 2026 08:13:28 -0000 @@ -301,6 +301,7 @@ roa_buffer(struct ibuf *b, const struct { io_simple_buffer(b, &roa->valid, sizeof(roa->valid)); io_simple_buffer(b, &roa->asid, sizeof(roa->asid)); + io_simple_buffer(b, &roa->issuerid, sizeof(roa->issuerid)); io_simple_buffer(b, &roa->talid, sizeof(roa->talid)); io_simple_buffer(b, &roa->num_ips, sizeof(roa->num_ips)); io_simple_buffer(b, &roa->expires, sizeof(roa->expires)); @@ -323,6 +324,7 @@ roa_read(struct ibuf *b) io_read_buf(b, &roa->valid, sizeof(roa->valid)); io_read_buf(b, &roa->asid, sizeof(roa->asid)); + io_read_buf(b, &roa->issuerid, sizeof(roa->issuerid)); io_read_buf(b, &roa->talid, sizeof(roa->talid)); io_read_buf(b, &roa->num_ips, sizeof(roa->num_ips)); io_read_buf(b, &roa->expires, sizeof(roa->expires)); Index: spl.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/spl.c,v diff -u -p -r1.20 spl.c --- spl.c 3 Sep 2026 17:19:30 -0000 1.20 +++ spl.c 5 Oct 2026 08:13:28 -0000 @@ -302,6 +302,7 @@ spl_buffer(struct ibuf *b, const struct { io_simple_buffer(b, &s->valid, sizeof(s->valid)); io_simple_buffer(b, &s->asid, sizeof(s->asid)); + io_simple_buffer(b, &s->issuerid, sizeof(s->issuerid)); io_simple_buffer(b, &s->talid, sizeof(s->talid)); io_simple_buffer(b, &s->num_prefixes, sizeof(s->num_prefixes)); io_simple_buffer(b, &s->expires, sizeof(s->expires)); @@ -325,6 +326,7 @@ spl_read(struct ibuf *b) io_read_buf(b, &s->valid, sizeof(s->valid)); io_read_buf(b, &s->asid, sizeof(s->asid)); + io_read_buf(b, &s->issuerid, sizeof(s->issuerid)); io_read_buf(b, &s->talid, sizeof(s->talid)); io_read_buf(b, &s->num_prefixes, sizeof(s->num_prefixes)); io_read_buf(b, &s->expires, sizeof(s->expires)); Index: output.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/output.c,v diff -u -p -r1.48 output.c --- output.c 29 Sep 2026 21:59:14 -0000 1.48 +++ output.c 5 Oct 2026 08:13:28 -0000 @@ -285,14 +285,16 @@ outputheader(FILE *out, struct validatio "# Route Origin Authorizations: %u (%u failed parse, %u invalid)\n" "# BGPsec Router Certificates: %u\n" "# Certificates: %u (%u invalid, %u non-functional, %u sync " - "deferred)\n", hn, tbuf, (long long)st->elapsed_time.tv_sec, + "deferred, %u unconfirmed)\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, - st->repo_tal_stats.certs_nonfunc_deferred) < 0) + st->repo_tal_stats.certs_nonfunc_deferred, + st->repo_tal_stats.certs_unconfirmed) < 0) return -1; if (fprintf(out, Index: output-json.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/output-json.c,v diff -u -p -r1.61 output-json.c --- output-json.c 7 Jul 2026 13:38:54 -0000 1.61 +++ output-json.c 5 Oct 2026 08:13:28 -0000 @@ -65,6 +65,7 @@ outputheader_json(struct validation_data json_do_int("invalidcertificates", st->repo_tal_stats.certs_fail); json_do_int("nonfunctionalcas", st->repo_tal_stats.certs_nonfunc); json_do_int("deferredcas", st->repo_tal_stats.certs_nonfunc_deferred); + json_do_int("unconfirmedcas", st->repo_tal_stats.certs_unconfirmed); json_do_int("taks", st->repo_tal_stats.taks); json_do_int("tals", st->tals); json_do_int("invalidtals", talsz - st->tals); Index: output-ometric.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/output-ometric.c,v diff -u -p -r1.17 output-ometric.c --- output-ometric.c 24 Jun 2026 09:06:20 -0000 1.17 +++ output-ometric.c 5 Oct 2026 08:13:28 -0000 @@ -46,6 +46,8 @@ set_common_stats(const struct repotalsta OKV("type", "state"), OKV("cert", "non-functional"), ol); ometric_set_int_with_labels(metric, in->certs_nonfunc_deferred, OKV("type", "state"), OKV("cert", "sync-deferred"), ol); + ometric_set_int_with_labels(metric, in->certs_unconfirmed, + OKV("type", "state"), OKV("cert", "unconfirmed"), ol); ometric_set_int_with_labels(metric, in->mfts, OKV("type", "state"), OKV("manifest", "valid"), ol);