Index | Thread | Search

From:
Job Snijders <job@openbsd.org>
Subject:
rpki-client: add support for draft-ietf-sidrops-rpki-prefixlist
To:
tech@openbsd.org
Date:
Tue, 20 Feb 2024 11:11:52 +0000

Download raw body.

Thread
This adds support for decoding and validating
draft-ietf-sidrops-rpki-prefixlist-02 objects. This implementation
requires the SPL prefixBlocks to be in canonical form, something we
hopefully can do for ROAs a few years down the road too.

Feedback welcome

Kind regards,

Job

Index: Makefile
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/Makefile,v
diff -u -p -r1.33 Makefile
--- Makefile	13 Oct 2023 12:06:49 -0000	1.33
+++ Makefile	20 Feb 2024 11:06:21 -0000
@@ -6,7 +6,7 @@ SRCS=	as.c aspa.c cert.c cms.c constrain
 	output.c output-bgpd.c output-bird.c output-csv.c output-json.c \
 	output-ometric.c parser.c print.c repo.c rfc3779.c roa.c \
 	rrdp.c rrdp_delta.c rrdp_notification.c rrdp_snapshot.c rrdp_util.c \
-	rsc.c rsync.c tak.c tal.c validate.c x509.c
+	rsc.c rsync.c spl.c tak.c tal.c validate.c x509.c
 MAN=	rpki-client.8
 
 LDADD+= -lexpat -ltls -lssl -lcrypto -lutil -lz
Index: extern.h
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/extern.h,v
diff -u -p -r1.204 extern.h
--- extern.h	16 Feb 2024 05:18:29 -0000	1.204
+++ extern.h	20 Feb 2024 11:06:22 -0000
@@ -178,6 +178,7 @@ enum rtype {
 	RTYPE_ASPA,
 	RTYPE_TAK,
 	RTYPE_GEOFEED,
+	RTYPE_SPL,
 };
 
 enum location {
@@ -281,6 +282,35 @@ struct rsc {
 };
 
 /*
+ * An IP address prefix for a given SPL.
+ * This encodes the AFI (v6/v4), and address.
+ */
+struct spl_pfx {
+	enum afi	 afi; /* AFI value */
+	struct ip_addr	 prefix; /* the address prefix itself */
+};
+
+/*
+ * An SPL, draft-ietf-sidrops-rpki-prefixlist
+ * This consists of an ASID and its IP prefixes.
+ */
+struct spl {
+	uint32_t	 asid; /* asID of SPL */
+	struct spl_pfx	*pfxs; /* IP prefixes */
+	size_t		 pfxsz; /* number of IP prefixes */
+	int		 talid; /* SPL is covered by which TAL */
+	int		 valid; /* validated the ASN */
+	char		*aia; /* AIA */
+	char		*aki; /* AKI */
+	char		*sia; /* SIA signedObject */
+	char		*ski; /* SKI */
+	time_t		 signtime; /* CMS signing-time attribute */
+	time_t		 notbefore; /* EE cert's Not Before */
+	time_t		 notafter; /* EE cert's Not After */
+	time_t		 expires; /* when the signature path expires */
+};
+
+/*
  * Datastructure representing the TAKey sequence inside TAKs.
  */
 struct takey {
@@ -410,6 +440,26 @@ RB_HEAD(vrp_tree, vrp);
 RB_PROTOTYPE(vrp_tree, vrp, entry, vrpcmp);
 
 /*
+ * Validated SignedPrefixList Payload
+ * A single VSP element (including ASID)
+ * draft-ietf-sidrops-rpki-prefixlist
+ */
+struct vsp {
+	RB_ENTRY(vsp)	 entry;
+	uint32_t	 asid;
+	struct spl_pfx	*prefixes;
+	size_t		 prefixesz;
+	time_t		 expires;
+	int		 talid;
+	unsigned int	 repoid;
+};
+/*
+ * Tree of VRP sorted by afi, addr, asid
+ */
+RB_HEAD(vsp_tree, vsp);
+RB_PROTOTYPE(vsp_tree, vsp, entry, vspcmp);
+
+/*
  * A single BGPsec Router Key (including ASID)
  */
 struct brk {
@@ -552,6 +602,9 @@ struct repotalstats {
 	uint32_t	 aspas; /* ASPA objects */
 	uint32_t	 aspas_fail; /* ASPA objects failing syntactic parse */
 	uint32_t	 aspas_invalid; /* ASPAs with invalid customerASID */
+	uint32_t	 spls; /* signed prefix list */
+	uint32_t	 spls_fail; /* failing syntactic parse */
+	uint32_t	 spls_invalid; /* invalid asid */
 	uint32_t	 brks; /* number of BGPsec Router Key (BRK) certs */
 	uint32_t	 crls; /* revocation lists */
 	uint32_t	 gbrs; /* ghostbuster records */
@@ -561,6 +614,8 @@ struct repotalstats {
 	uint32_t	 vaps_pas; /* total number of providers */
 	uint32_t	 vrps; /* total number of Validated ROA Payloads */
 	uint32_t	 vrps_uniqs; /* number of unique vrps */
+	uint32_t	 vsps; /* total number of Validated SPL Payloads */
+	uint32_t	 vsps_uniqs; /* number of unique vsps */
 };
 
 struct repostats {
@@ -596,6 +651,7 @@ struct msgbuf;
 extern int verbose;
 extern int filemode;
 extern int excludeaspa;
+extern int excludespl;
 extern const char *tals[];
 extern const char *taldescs[];
 extern unsigned int talrepocnt[];
@@ -637,6 +693,14 @@ struct roa	*roa_read(struct ibuf *);
 void		 roa_insert_vrps(struct vrp_tree *, struct roa *,
 		    struct repo *);
 
+void		 spl_buffer(struct ibuf *, const struct spl *);
+void		 spl_free(struct spl *);
+struct spl	*spl_parse(X509 **, const char *, int, const unsigned char *,
+		    size_t);
+struct spl	*spl_read(struct ibuf *);
+void		 spl_insert_vsps(struct vsp_tree *, struct spl *,
+		    struct repo *);
+
 void		 gbr_free(struct gbr *);
 struct gbr	*gbr_parse(X509 **, const char *, int, const unsigned char *,
 		    size_t);
@@ -691,6 +755,7 @@ int		 valid_aspa(const char *, struct ce
 int		 valid_geofeed(const char *, struct cert *, struct geofeed *);
 int		 valid_uuid(const char *);
 int		 valid_ca_pkey(const char *, EVP_PKEY *);
+int		 valid_spl(const char *, struct cert *, struct spl *);
 
 /* Working with CMS. */
 unsigned char	*cms_parse_validate(X509 **, const char *,
@@ -872,6 +937,7 @@ void		 rsc_print(const X509 *, const str
 void		 aspa_print(const X509 *, const struct aspa *);
 void		 tak_print(const X509 *, const struct tak *);
 void		 geofeed_print(const X509 *, const struct geofeed *);
+void		 spl_print(const X509 *, const struct spl *);
 
 /* Missing RFC 3779 API */
 IPAddrBlocks *IPAddrBlocks_new(void);
@@ -887,22 +953,22 @@ extern int	 outformats;
 #define FORMAT_OMETRIC	0x10
 
 int		 outputfiles(struct vrp_tree *v, struct brk_tree *b,
-		    struct vap_tree *, struct stats *);
+		    struct vap_tree *, struct vsp_tree *, struct stats *);
 int		 outputheader(FILE *, struct stats *);
 int		 output_bgpd(FILE *, struct vrp_tree *, struct brk_tree *,
-		    struct vap_tree *, struct stats *);
+		    struct vap_tree *, struct vsp_tree *, struct stats *);
 int		 output_bird1v4(FILE *, struct vrp_tree *, struct brk_tree *,
-		    struct vap_tree *, struct stats *);
+		    struct vap_tree *, struct vsp_tree *, struct stats *);
 int		 output_bird1v6(FILE *, struct vrp_tree *, struct brk_tree *,
-		    struct vap_tree *, struct stats *);
+		    struct vap_tree *, struct vsp_tree *, struct stats *);
 int		 output_bird2(FILE *, struct vrp_tree *, struct brk_tree *,
-		    struct vap_tree *, struct stats *);
+		    struct vap_tree *, struct vsp_tree *, struct stats *);
 int		 output_csv(FILE *, struct vrp_tree *, struct brk_tree *,
-		    struct vap_tree *, struct stats *);
+		    struct vap_tree *, struct vsp_tree *, struct stats *);
 int		 output_json(FILE *, struct vrp_tree *, struct brk_tree *,
-		    struct vap_tree *, struct stats *);
+		    struct vap_tree *, struct vsp_tree *, struct stats *);
 int		 output_ometric(FILE *, struct vrp_tree *, struct brk_tree *,
-		    struct vap_tree *, struct stats *);
+		    struct vap_tree *, struct vsp_tree *, struct stats *);
 
 void		logx(const char *fmt, ...)
 		    __attribute__((format(printf, 1, 2)));
Index: filemode.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/filemode.c,v
diff -u -p -r1.37 filemode.c
--- filemode.c	23 Jan 2024 09:32:57 -0000	1.37
+++ filemode.c	20 Feb 2024 11:06:22 -0000
@@ -296,6 +296,7 @@ proc_parser_file(char *file, unsigned ch
 	struct mft *mft = NULL;
 	struct roa *roa = NULL;
 	struct rsc *rsc = NULL;
+	struct spl *spl = NULL;
 	struct tak *tak = NULL;
 	struct tal *tal = NULL;
 	char *aia = NULL, *aki = NULL;
@@ -422,6 +423,15 @@ proc_parser_file(char *file, unsigned ch
 		expires = &rsc->expires;
 		notafter = &rsc->notafter;
 		break;
+	case RTYPE_SPL:
+		spl = spl_parse(&x509, file, -1, buf, len);
+		if (spl == NULL)
+			break;
+		aia = spl->aia;
+		aki = spl->aki;
+		expires = &spl->expires;
+		notafter = &spl->notafter;
+		break;
 	case RTYPE_TAK:
 		tak = tak_parse(&x509, file, -1, buf, len);
 		if (tak == NULL)
@@ -464,6 +474,8 @@ proc_parser_file(char *file, unsigned ch
 			case RTYPE_RSC:
 				status = rsc->valid;
 				break;
+			case RTYPE_SPL:
+				status = spl->valid;
 			default:
 				break;
 			}
@@ -522,6 +534,9 @@ proc_parser_file(char *file, unsigned ch
 			break;
 		case RTYPE_RSC:
 			rsc_print(x509, rsc);
+			break;
+		case RTYPE_SPL:
+			spl_print(x509, spl);
 			break;
 		case RTYPE_TAK:
 			tak_print(x509, tak);
Index: main.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/main.c,v
diff -u -p -r1.249 main.c
--- main.c	16 Feb 2024 11:55:42 -0000	1.249
+++ main.c	20 Feb 2024 11:06:22 -0000
@@ -68,6 +68,7 @@ const char	*bird_tablename = "ROAS";
 int	verbose;
 int	noop;
 int	excludeaspa;
+int	excludespl;
 int	filemode;
 int	shortlistmode;
 int	rrdpon = 1;
@@ -548,7 +549,8 @@ queue_add_from_cert(const struct cert *c
  */
 static void
 entity_process(struct ibuf *b, struct stats *st, struct vrp_tree *tree,
-    struct brk_tree *brktree, struct vap_tree *vaptree)
+    struct brk_tree *brktree, struct vap_tree *vaptree,
+    struct vsp_tree *vsptree)
 {
 	enum rtype	 type;
 	struct tal	*tal;
@@ -556,6 +558,7 @@ entity_process(struct ibuf *b, struct st
 	struct mft	*mft;
 	struct roa	*roa;
 	struct aspa	*aspa;
+	struct spl	*spl;
 	struct repo	*rp;
 	char		*file;
 	time_t		 mtime;
@@ -656,6 +659,19 @@ entity_process(struct ibuf *b, struct st
 			repo_stat_inc(rp, talid, type, STYPE_INVALID);
 		aspa_free(aspa);
 		break;
+	case RTYPE_SPL:
+		io_read_buf(b, &c, sizeof(c));
+		if (c == 0) {
+			repo_stat_inc(rp, talid, type, STYPE_FAIL);
+			break;
+		}
+		spl = spl_read(b);
+		if (spl->valid)
+			spl_insert_vsps(vsptree, spl, rp);
+		else
+			repo_stat_inc(rp, talid, type, STYPE_INVALID);
+		spl_free(spl);
+		break;
 	case RTYPE_TAK:
 		break;
 	case RTYPE_FILE:
@@ -740,6 +756,9 @@ sum_stats(const struct repo *rp, const s
 	out->brks += in->brks;
 	out->crls += in->crls;
 	out->gbrs += in->gbrs;
+	out->spls += in->spls;
+	out->spls_fail += in->spls_fail;
+	out->spls_invalid += in->spls_invalid;
 	out->taks += in->taks;
 	out->vrps += in->vrps;
 	out->vrps_uniqs += in->vrps_uniqs;
@@ -938,6 +957,7 @@ main(int argc, char *argv[])
 	const char	*errs, *name;
 	const char	*skiplistfile = NULL;
 	struct vrp_tree	 vrps = RB_INITIALIZER(&vrps);
+	struct vsp_tree	 vsps = RB_INITIALIZER(&vsps);
 	struct brk_tree	 brks = RB_INITIALIZER(&brks);
 	struct vap_tree	 vaps = RB_INITIALIZER(&vaps);
 	struct rusage	 ru;
@@ -966,7 +986,7 @@ main(int argc, char *argv[])
 	    "proc exec unveil", NULL) == -1)
 		err(1, "pledge");
 
-	while ((c = getopt(argc, argv, "Ab:Bcd:e:fH:jmnoP:rRs:S:t:T:vV")) != -1)
+	while ((c = getopt(argc, argv, "Ab:Bcd:e:fH:jmnopP:rRs:S:t:T:vV")) != -1)
 		switch (c) {
 		case 'A':
 			excludeaspa = 1;
@@ -1006,6 +1026,9 @@ main(int argc, char *argv[])
 		case 'o':
 			outformats |= FORMAT_OPENBGPD;
 			break;
+		case 'p':
+			excludespl = 1;
+			break;
 		case 'P':
 			evaluation_time = strtonum(optarg, X509_TIME_MIN + 1,
 			    X509_TIME_MAX, &errs);
@@ -1332,7 +1355,8 @@ main(int argc, char *argv[])
 		if ((pfd[0].revents & POLLIN)) {
 			b = io_buf_read(proc, &procbuf);
 			if (b != NULL) {
-				entity_process(b, &stats, &vrps, &brks, &vaps);
+				entity_process(b, &stats, &vrps, &brks, &vaps,
+				    &vsps);
 				ibuf_free(b);
 			}
 		}
@@ -1425,7 +1449,7 @@ main(int argc, char *argv[])
 	}
 	repo_stats_collect(sum_repostats, &stats.repo_stats);
 
-	if (outputfiles(&vrps, &brks, &vaps, &stats))
+	if (outputfiles(&vrps, &brks, &vaps, &vsps, &stats))
 		rc = 1;
 
 	printf("Processing time %lld seconds "
@@ -1442,6 +1466,9 @@ main(int argc, char *argv[])
 	    "invalid)\n", stats.repo_tal_stats.aspas,
 	    stats.repo_tal_stats.aspas_fail,
 	    stats.repo_tal_stats.aspas_invalid);
+	printf("Signed Prefix Lists: %u (%u failed parse, %u invalid)\n",
+	    stats.repo_tal_stats.spls, stats.repo_tal_stats.spls_fail,
+	    stats.repo_tal_stats.spls_invalid);
 	printf("BGPsec Router Certificates: %u\n", stats.repo_tal_stats.brks);
 	printf("Certificates: %u (%u invalid)\n",
 	    stats.repo_tal_stats.certs, stats.repo_tal_stats.certs_fail);
Index: mft.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/mft.c,v
diff -u -p -r1.110 mft.c
--- mft.c	16 Feb 2024 15:18:08 -0000	1.110
+++ mft.c	20 Feb 2024 11:06:22 -0000
@@ -130,6 +130,8 @@ rtype_from_file_extension(const char *fn
 		return RTYPE_TAK;
 	if (strcasecmp(fn + sz - 4, ".csv") == 0)
 		return RTYPE_GEOFEED;
+	if (strcasecmp(fn + sz - 4, ".spl") == 0)
+		return RTYPE_SPL;
 
 	return RTYPE_INVALID;
 }
@@ -166,11 +168,12 @@ rtype_from_mftfile(const char *fn)
 
 	type = rtype_from_file_extension(fn);
 	switch (type) {
+	case RTYPE_ASPA:
 	case RTYPE_CER:
 	case RTYPE_CRL:
 	case RTYPE_GBR:
 	case RTYPE_ROA:
-	case RTYPE_ASPA:
+	case RTYPE_SPL:
 	case RTYPE_TAK:
 		return type;
 	default:
Index: output-bgpd.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-bgpd.c,v
diff -u -p -r1.28 output-bgpd.c
--- output-bgpd.c	26 Jun 2023 18:39:53 -0000	1.28
+++ output-bgpd.c	20 Feb 2024 11:06:22 -0000
@@ -21,7 +21,7 @@
 
 int
 output_bgpd(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct vap_tree *vaps, struct stats *st)
+    struct vap_tree *vaps, struct vsp_tree *vsps, struct stats *st)
 {
 	struct vrp	*vrp;
 	struct vap	*vap;
Index: output-bird.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-bird.c,v
diff -u -p -r1.18 output-bird.c
--- output-bird.c	30 May 2023 12:14:48 -0000	1.18
+++ output-bird.c	20 Feb 2024 11:06:22 -0000
@@ -22,7 +22,7 @@
 
 int
 output_bird1v4(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct vap_tree *vaps, struct stats *st)
+    struct vap_tree *vaps, struct vsp_tree *vsps, struct stats *st)
 {
 	extern		const char *bird_tablename;
 	struct vrp	*v;
@@ -51,7 +51,7 @@ output_bird1v4(FILE *out, struct vrp_tre
 
 int
 output_bird1v6(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct vap_tree *vaps, struct stats *st)
+    struct vap_tree *vaps, struct vsp_tree *vsps, struct stats *st)
 {
 	extern		const char *bird_tablename;
 	struct vrp	*v;
@@ -80,7 +80,7 @@ output_bird1v6(FILE *out, struct vrp_tre
 
 int
 output_bird2(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct vap_tree *vaps, struct stats *st)
+    struct vap_tree *vaps, struct vsp_tree *vsps, struct stats *st)
 {
 	extern		const char *bird_tablename;
 	struct vrp	*v;
Index: output-csv.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-csv.c,v
diff -u -p -r1.13 output-csv.c
--- output-csv.c	30 Aug 2022 18:56:49 -0000	1.13
+++ output-csv.c	20 Feb 2024 11:06:22 -0000
@@ -21,7 +21,7 @@
 
 int
 output_csv(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct vap_tree *vaps, struct stats *st)
+    struct vap_tree *vaps, struct vsp_tree *vsps, struct stats *st)
 {
 	struct vrp	*v;
 
Index: output-json.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-json.c,v
diff -u -p -r1.42 output-json.c
--- output-json.c	13 Feb 2024 20:41:22 -0000	1.42
+++ output-json.c	20 Feb 2024 11:06:22 -0000
@@ -47,6 +47,9 @@ outputheader_json(struct stats *st)
 	json_do_int("roas", st->repo_tal_stats.roas);
 	json_do_int("failedroas", st->repo_tal_stats.roas_fail);
 	json_do_int("invalidroas", st->repo_tal_stats.roas_invalid);
+	json_do_int("spls", st->repo_tal_stats.spls);
+	json_do_int("failedspls", st->repo_tal_stats.spls_fail);
+	json_do_int("invalidspls", st->repo_tal_stats.spls_invalid);
 	json_do_int("aspas", st->repo_tal_stats.aspas);
 	json_do_int("failedaspas", st->repo_tal_stats.aspas_fail);
 	json_do_int("invalidaspas", st->repo_tal_stats.aspas_invalid);
@@ -69,6 +72,8 @@ outputheader_json(struct stats *st)
 	json_do_int("repositories", st->repos);
 	json_do_int("vrps", st->repo_tal_stats.vrps);
 	json_do_int("uniquevrps", st->repo_tal_stats.vrps_uniqs);
+	json_do_int("vsps", st->repo_tal_stats.vsps);
+	json_do_int("uniquevsps", st->repo_tal_stats.vsps_uniqs);
 	json_do_int("vaps", st->repo_tal_stats.vaps);
 	json_do_int("uniquevaps", st->repo_tal_stats.vaps_uniqs);
 	json_do_int("cachedir_del_files", st->repo_stats.del_files);
@@ -109,11 +114,12 @@ output_aspa(struct vap_tree *vaps)
 
 int
 output_json(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct vap_tree *vaps, struct stats *st)
+    struct vap_tree *vaps, struct vsp_tree *vsps, struct stats *st)
 {
 	char		 buf[64];
 	struct vrp	*v;
 	struct brk	*b;
+	struct vsp	*vsp;
 
 	json_do_start(out);
 	outputheader_json(st);
@@ -146,6 +152,27 @@ output_json(FILE *out, struct vrp_tree *
 
 	if (!excludeaspa)
 		output_aspa(vaps);
+
+	if (!excludespl) {
+		size_t i;
+
+		json_do_array("signedprefixlists");
+		RB_FOREACH(vsp, vsp_tree, vsps) {
+			json_do_object("vsp", 1);
+			json_do_int("origin_asn", vsp->asid);
+			json_do_array("prefixes");
+			for (i = 0; i < vsp->prefixesz; i++) {
+				ip_addr_print(&vsp->prefixes[i].prefix,
+				    vsp->prefixes[i].afi, buf, sizeof(buf));
+				json_do_string("prefix", buf);
+			}
+			json_do_end();
+			json_do_int("expires", vsp->expires);
+			json_do_string("ta", taldescs[vsp->talid]);
+			json_do_end();
+		}
+		json_do_end();
+	}
 
 	return json_do_finish();
 }
Index: output-ometric.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-ometric.c,v
diff -u -p -r1.7 output-ometric.c
--- output-ometric.c	13 Feb 2024 20:41:22 -0000	1.7
+++ output-ometric.c	20 Feb 2024 11:06:22 -0000
@@ -146,7 +146,7 @@ repo_stats(const struct repo *rp, const 
 
 int
 output_ometric(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks,
-    struct vap_tree *vaps, struct stats *st)
+    struct vap_tree *vaps, struct vsp_tree *vsps, struct stats *st)
 {
 	struct olabels *ol;
 	const char *keys[4] = { "nodename", "domainname", "release", NULL };
Index: output.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output.c,v
diff -u -p -r1.32 output.c
--- output.c	3 Feb 2024 14:30:47 -0000	1.32
+++ output.c	20 Feb 2024 11:06:22 -0000
@@ -64,7 +64,7 @@ static const struct outputs {
 	int	 format;
 	char	*name;
 	int	(*fn)(FILE *, struct vrp_tree *, struct brk_tree *,
-		    struct vap_tree *, struct stats *);
+		    struct vap_tree *, struct vsp_tree *, struct stats *);
 } outputs[] = {
 	{ FORMAT_OPENBGPD, "openbgpd", output_bgpd },
 	{ FORMAT_BIRD, "bird1v4", output_bird1v4 },
@@ -84,7 +84,7 @@ static void	 set_signal_handler(void);
 
 int
 outputfiles(struct vrp_tree *v, struct brk_tree *b, struct vap_tree *a,
-    struct stats *st)
+    struct vsp_tree *p, struct stats *st)
 {
 	int i, rc = 0;
 
@@ -103,7 +103,7 @@ outputfiles(struct vrp_tree *v, struct b
 			rc = 1;
 			continue;
 		}
-		if ((*outputs[i].fn)(fout, v, b, a, st) != 0) {
+		if ((*outputs[i].fn)(fout, v, b, a, p, st) != 0) {
 			warn("output for %s format failed", outputs[i].name);
 			fclose(fout);
 			output_cleantmp();
Index: parser.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/parser.c,v
diff -u -p -r1.128 parser.c
--- parser.c	3 Feb 2024 14:30:47 -0000	1.128
+++ parser.c	20 Feb 2024 11:06:22 -0000
@@ -158,6 +158,41 @@ proc_parser_roa(char *file, const unsign
 }
 
 /*
+ * Parse and validate a draft-ietf-sidrops-rpki-prefixlist SPL.
+ * Returns the spl on success, NULL on failure.
+ */
+static struct spl *
+proc_parser_spl(char *file, const unsigned char *der, size_t len,
+    const struct entity *entp)
+{
+	struct spl		*spl;
+	struct auth		*a;
+	struct crl		*crl;
+	X509			*x509;
+	const char		*errstr;
+
+	if ((spl = spl_parse(&x509, file, entp->talid, der, len)) == NULL)
+		return NULL;
+
+	a = valid_ski_aki(file, &auths, spl->ski, spl->aki, entp->mftaki);
+	crl = crl_get(&crlt, a);
+
+	if (!valid_x509(file, ctx, x509, a, crl, &errstr)) {
+		warnx("%s: %s", file, errstr);
+		X509_free(x509);
+		spl_free(spl);
+		return NULL;
+	}
+	X509_free(x509);
+
+	spl->talid = a->cert->talid;
+
+	spl->expires = x509_find_expires(spl->notafter, a, &crlt);
+
+	return spl;
+}
+
+/*
  * Check all files and their hashes in a MFT structure.
  * Return zero on failure, non-zero on success.
  */
@@ -681,6 +716,7 @@ parse_entity(struct entityq *q, struct m
 	struct aspa	*aspa;
 	struct gbr	*gbr;
 	struct tak	*tak;
+	struct spl	*spl;
 	struct ibuf	*b;
 	unsigned char	*f;
 	time_t		 mtime, crlmtime;
@@ -821,6 +857,19 @@ parse_entity(struct entityq *q, struct m
 				mtime = tak->signtime;
 			io_simple_buffer(b, &mtime, sizeof(mtime));
 			tak_free(tak);
+			break;
+		case RTYPE_SPL:
+			file = parse_load_file(entp, &f, &flen);
+			io_str_buffer(b, file);
+			spl = proc_parser_spl(file, f, flen, entp);
+			if (spl != NULL)
+				mtime = spl->signtime;
+			io_simple_buffer(b, &mtime, sizeof(mtime));
+			c = (spl != NULL);
+			io_simple_buffer(b, &c, sizeof(int));
+			if (spl != NULL)
+				spl_buffer(b, spl);
+			spl_free(spl);
 			break;
 		case RTYPE_CRL:
 		default:
Index: print.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/print.c,v
diff -u -p -r1.49 print.c
--- print.c	16 Feb 2024 05:18:29 -0000	1.49
+++ print.c	20 Feb 2024 11:06:22 -0000
@@ -508,6 +508,60 @@ roa_print(const X509 *x, const struct ro
 }
 
 void
+spl_print(const X509 *x, const struct spl *s)
+{
+	char	 buf[128];
+	size_t	 i;
+
+	if (outformats & FORMAT_JSON) {
+		json_do_string("type", "spl");
+		json_do_string("ski", pretty_key_id(s->ski));
+		x509_print(x);
+		json_do_string("aki", pretty_key_id(s->aki));
+		json_do_string("aia", s->aia);
+		json_do_string("sia", s->sia);
+		if (s->signtime != 0)
+			json_do_int("signing_time", s->signtime);
+		json_do_int("valid_since", s->notbefore);
+		json_do_int("valid_until", s->notafter);
+		if (s->expires)
+			json_do_int("expires", s->expires);
+		json_do_int("asid", s->asid);
+	} else {
+		printf("Subject key identifier:   %s\n", pretty_key_id(s->ski));
+		x509_print(x);
+		printf("Authority key identifier: %s\n", pretty_key_id(s->aki));
+		printf("Authority info access:    %s\n", s->aia);
+		printf("Subject info access:      %s\n", s->sia);
+		if (s->signtime != 0)
+			printf("Signing time:             %s\n",
+			    time2str(s->signtime));
+		printf("SPL not before:           %s\n",
+		    time2str(s->notbefore));
+		printf("SPL not after:            %s\n", time2str(s->notafter));
+		printf("asID:                     %u\n", s->asid);
+		printf("Originated IP Prefixes:   ");
+	}
+
+	if (outformats & FORMAT_JSON)
+		json_do_array("prefixes");
+	for (i = 0; i < s->pfxsz; i++) {
+		ip_addr_print(&s->pfxs[i].prefix, s->pfxs[i].afi, buf,
+		    sizeof(buf));
+
+		if (outformats & FORMAT_JSON) {
+			json_do_string("prefix", buf);
+		} else {
+			if (i > 0)
+				printf("%26s", "");
+			printf("%s\n", buf); 
+		}
+	}
+	if (outformats & FORMAT_JSON)
+		json_do_end();
+}
+
+void
 gbr_print(const X509 *x, const struct gbr *p)
 {
 	if (outformats & FORMAT_JSON) {
Index: repo.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/repo.c,v
diff -u -p -r1.52 repo.c
--- repo.c	3 Feb 2024 14:30:47 -0000	1.52
+++ repo.c	20 Feb 2024 11:06:22 -0000
@@ -1480,6 +1480,30 @@ repo_stat_inc(struct repo *rp, int talid
 			break;
 		}
 		break;
+	case RTYPE_SPL:
+		switch (subtype) {
+		case STYPE_OK:
+			rp->stats[talid].spls++;
+			break;
+		case STYPE_FAIL:
+			rp->stats[talid].spls_fail++;
+			break;
+		case STYPE_INVALID:
+			rp->stats[talid].spls_invalid++;
+			break;
+		case STYPE_TOTAL:
+			rp->stats[talid].vsps++;
+			break;
+		case STYPE_UNIQUE:
+			rp->stats[talid].vsps_uniqs++;
+			break;
+		case STYPE_DEC_UNIQUE:
+			rp->stats[talid].vsps_uniqs--;
+			break;
+		default:
+			break;
+		}
+		break;
 	case RTYPE_CRL:
 		rp->stats[talid].crls++;
 		break;
Index: rpki-client.8
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/rpki-client.8,v
diff -u -p -r1.100 rpki-client.8
--- rpki-client.8	31 Jan 2024 17:19:02 -0000	1.100
+++ rpki-client.8	20 Feb 2024 11:06:22 -0000
@@ -451,6 +451,12 @@ agreement regarding ARIN service restric
 .%U https://datatracker.ietf.org/doc/html/draft-spaghetti-sidrops-rrdp-desynchronization-00
 .%D Jan, 2024
 .Re
+.Pp
+.Rs
+.%T A profile for Signed Prefix Lists for Use in the Resource Public Key Infrastructure (RPKI)
+.%U https://datatracker.ietf.org/doc/html/draft-ietf-sidrops-rpki-prefixlist-02
+.%D Jan, 2024
+.Re
 .Sh HISTORY
 .Nm
 first appeared in
Index: spl.c
===================================================================
RCS file: spl.c
diff -N spl.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ spl.c	20 Feb 2024 11:06:22 -0000
@@ -0,0 +1,475 @@
+/*	$OpenBSD: spl.c,v 1.72 2023/12/14 07:52:53 tb Exp $ */
+/*
+ * Copyright (c) 2024 Job Snijders <job@fastly.com>
+ * Copyright (c) 2022 Theo Buehler <tb@openbsd.org>
+ * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv>
+ *
+ * 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 <assert.h>
+#include <err.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <openssl/asn1.h>
+#include <openssl/asn1t.h>
+#include <openssl/stack.h>
+#include <openssl/safestack.h>
+#include <openssl/x509.h>
+#include <openssl/x509v3.h>
+
+#include "extern.h"
+
+extern ASN1_OBJECT	*spl_oid;
+
+/*
+ * Types and templates for the SPL eContent.
+ */
+
+ASN1_ITEM_EXP AddressFamilyPrefixes_it;
+ASN1_ITEM_EXP SignedPrefixList_it;
+
+typedef struct {
+	ASN1_OCTET_STRING		*addressFamily;
+	STACK_OF(ASN1_BIT_STRING)	*addressPrefixes;
+} AddressFamilyPrefixes;
+
+DECLARE_STACK_OF(AddressFamilyPrefixes);
+
+ASN1_SEQUENCE(AddressFamilyPrefixes) = {
+	ASN1_SIMPLE(AddressFamilyPrefixes, addressFamily, ASN1_OCTET_STRING),
+	ASN1_SEQUENCE_OF(AddressFamilyPrefixes, addressPrefixes,
+	    ASN1_BIT_STRING),
+} ASN1_SEQUENCE_END(AddressFamilyPrefixes);
+
+#ifndef DEFINE_STACK_OF
+#define sk_ASN1_BIT_STRING_num(st)	SKM_sk_num(ASN1_BIT_STRING, (st))
+#define sk_ASN1_BIT_STRING_value(st, i)	SKM_sk_value(ASN1_BIT_STRING, (st), (i))
+
+#define sk_AddressFamilyPrefixes_num(st)	\
+    SKM_sk_num(AddressFamilyPrefixes, (st))
+#define sk_AddressFamilyPrefixes_value(st, i)	\
+    SKM_sk_value(AddressFamilyPrefixes, (st), (i))
+#endif
+
+typedef struct {
+	ASN1_INTEGER			*version;
+	ASN1_INTEGER			*asid;
+	STACK_OF(AddressFamilyPrefixes)	*prefixBlocks;
+} SignedPrefixList;
+
+ASN1_SEQUENCE(SignedPrefixList) = {
+	ASN1_EXP_OPT(SignedPrefixList, version, ASN1_INTEGER, 0),
+	ASN1_SIMPLE(SignedPrefixList, asid, ASN1_INTEGER),
+	ASN1_SEQUENCE_OF(SignedPrefixList, prefixBlocks, AddressFamilyPrefixes)
+} ASN1_SEQUENCE_END(SignedPrefixList);
+
+DECLARE_ASN1_FUNCTIONS(SignedPrefixList);
+IMPLEMENT_ASN1_FUNCTIONS(SignedPrefixList);
+
+/*
+ * Comparator to help sorting elements in SPL prefixBlocks and VSPs.
+ * Similar to rfc6482bis section 4.3.3 "Canonical form for ipAddrBlocks"
+ * for ROAs, except maxLength is omitted.
+ * Returns -1 if 'a' should precede 'b', 1 if 'b' should precede 'a',
+ * or '0' if a and b are equal.
+ */ 
+static int
+spl_addr_cmp(enum afi afi, struct ip_addr *a, struct ip_addr *b)
+{
+	int rv;
+
+	if (a == NULL)
+		return -1;
+
+	/*  lower addr value precede data elements with a higher addr value */
+	switch (afi) {
+	case AFI_IPV4:
+		rv = memcmp(&a->addr, &b->addr, 4);
+		if (rv)
+			return rv;
+		break;
+	case AFI_IPV6:
+		rv = memcmp(&a->addr, &b->addr, 16);
+		if (rv)
+			return rv;
+		break;
+	default:
+		break;
+	}
+
+	/* smaller prefixlen will precede longer, e.g. /8 vs /10 */
+	if (a->prefixlen < b->prefixlen)
+		return -1;
+	if (a->prefixlen > b->prefixlen)
+		return 1;
+
+	return 0;
+}
+
+/*
+ * Parses the eContent section of a SPL file,
+ * draft-ietf-sidrops-rpki-prefixlist-02 section 3.
+ * Returns zero on failure, non-zero on success.
+ */
+static int
+spl_parse_econtent(const char *fn, struct spl *spl, const unsigned char *d,
+    size_t dsz)
+{
+	const unsigned char		*oder;
+	SignedPrefixList		*spl_asn1;
+	const AddressFamilyPrefixes	*afp;
+	const STACK_OF(ASN1_BIT_STRING)	*prefixes;
+	const ASN1_BIT_STRING		*prefix_asn1;
+	int				 afpsz, prefixesz;
+	enum afi			 afi;
+	struct ip_addr			 ip_addr;
+	struct spl_pfx			*res;
+	int				 ipv4_seen = 0, ipv6_seen = 0;
+	int				 i, j, rc = 0;
+
+	oder = d;
+	if ((spl_asn1 = d2i_SignedPrefixList(NULL, &d, dsz)) == NULL) {
+		warnx("%s: RFC 6482 section 3: failed to parse "
+		    "SignedPrefixList", fn);
+		goto out;
+	}
+	if (d != oder + dsz) {
+		warnx("%s: %td bytes trailing garbage in eContent", fn,
+		    oder + dsz - d);
+		goto out;
+	}
+
+	if (!valid_econtent_version(fn, spl_asn1->version, 0))
+		goto out;
+
+	if (!as_id_parse(spl_asn1->asid, &spl->asid)) {
+		warnx("%s: asid: malformed AS identifier", fn);
+		goto out;
+	}
+
+	afpsz = sk_AddressFamilyPrefixes_num(spl_asn1->prefixBlocks);
+	if (afpsz != 0 && afpsz != 1 && afpsz != 2) {
+		warnx("%s: unexpected number of AddressFamilyAddressPrefixes"
+		    "(got %d, expected 0, 1, or 2)", fn, afpsz);
+		goto out;
+	}
+
+	for (i = 0; i < afpsz; i++) {
+		struct ip_addr *prev_ip_addr = NULL;
+
+		afp = sk_AddressFamilyPrefixes_value(spl_asn1->prefixBlocks, i);
+		prefixes = afp->addressPrefixes;
+		prefixesz = sk_ASN1_BIT_STRING_num(afp->addressPrefixes);
+
+		if (prefixesz == 0) {
+			warnx("%s: empty AddressFamilyAddressPrefixes", fn);
+			goto out;
+		}
+		if (spl->pfxsz + prefixesz >= MAX_IP_SIZE) {
+			warnx("%s: too many addressPrefixes entries", fn);
+			goto out;
+		}
+
+		if (!ip_addr_afi_parse(fn, afp->addressFamily, &afi))
+			goto out;
+
+		switch (afi) {
+		case AFI_IPV4:
+			if (ipv4_seen++ > 0) {
+				warnx("%s: addressFamilyIPv4 appeared twice",
+				   fn);
+				goto out;
+			}
+			break;
+		case AFI_IPV6:
+			if (ipv6_seen++ > 0) {
+				warnx("%s: addressFamilyIPv6 appeared twice",
+				    fn);
+				goto out;
+			}
+		}
+
+		spl->pfxs = recallocarray(spl->pfxs, spl->pfxsz,
+		    spl->pfxsz + prefixesz, sizeof(struct spl_pfx));
+		if (spl->pfxs == NULL)
+			err(1, NULL);
+
+		for (j = 0; j < prefixesz; j++) {
+			prefix_asn1 = sk_ASN1_BIT_STRING_value(prefixes, j);
+
+			if (!ip_addr_parse(prefix_asn1, afi, fn, &ip_addr)) {
+				warnx("%s: invalid AddressPrefix", fn);
+				goto out;
+			}
+		
+			if (spl_addr_cmp(afi, prev_ip_addr, &ip_addr) != -1) {
+				warnx("%s: invalid addressPrefixes sorting", fn);
+				goto out;
+			}
+
+			res = &spl->pfxs[spl->pfxsz++];
+			res->prefix = ip_addr;
+			res->afi = afi;
+			prev_ip_addr = &res->prefix;
+		}
+	}
+
+	rc = 1;
+ out:
+	SignedPrefixList_free(spl_asn1);
+	return rc;
+}
+
+/*
+ * Parse a full Signed Prefix List file.
+ * Returns the SPL, or NULL if the object was malformed.
+ */
+struct spl *
+spl_parse(X509 **x509, const char *fn, int talid, const unsigned char *der,
+    size_t len)
+{
+	struct spl	*spl;
+	size_t		 cmsz;
+	unsigned char	*cms;
+	struct cert	*cert = NULL;
+	time_t		 signtime = 0;
+	int		 rc = 0;
+
+	cms = cms_parse_validate(x509, fn, der, len, spl_oid, &cmsz, &signtime);
+	if (cms == NULL)
+		return NULL;
+
+	if ((spl = calloc(1, sizeof(*spl))) == NULL)
+		err(1, NULL);
+	spl->signtime = signtime;
+
+	if (!x509_get_aia(*x509, fn, &spl->aia))
+		goto out;
+	if (!x509_get_aki(*x509, fn, &spl->aki))
+		goto out;
+	if (!x509_get_sia(*x509, fn, &spl->sia))
+		goto out;
+	if (!x509_get_ski(*x509, fn, &spl->ski))
+		goto out;
+	if (spl->aia == NULL || spl->aki == NULL || spl->sia == NULL ||
+	    spl->ski == NULL) {
+		warnx("%s: RFC 6487 section 4.8: "
+		    "missing AIA, AKI, SIA, or SKI X509 extension", fn);
+		goto out;
+	}
+
+	if (!x509_get_notbefore(*x509, fn, &spl->notbefore))
+		goto out;
+	if (!x509_get_notafter(*x509, fn, &spl->notafter))
+		goto out;
+
+	if (!spl_parse_econtent(fn, spl, cms, cmsz))
+		goto out;
+
+	if (x509_any_inherits(*x509)) {
+		warnx("%s: inherit elements not allowed in EE cert", fn);
+		goto out;
+	}
+
+	if ((cert = cert_parse_ee_cert(fn, talid, *x509)) == NULL)
+		goto out;
+
+	if (cert->ipsz > 0) {
+		warnx("%s: superfluous IP Resources extension present", fn);
+		goto out;
+	}
+
+	/*
+	 * If the SPL isn't valid, we accept it anyway and depend upon
+	 * the code around spl_read() to check the "valid" field itself.
+	 */
+	spl->valid = valid_spl(fn, cert, spl);
+
+	rc = 1;
+ out:
+	if (rc == 0) {
+		spl_free(spl);
+		spl = NULL;
+		X509_free(*x509);
+		*x509 = NULL;
+	}
+	cert_free(cert);
+	free(cms);
+	return spl;
+}
+
+/*
+ * Free memory following a SPL pointer.
+ * Safe to call with NULL.
+ */
+void
+spl_free(struct spl *s)
+{
+	if (s == NULL)
+		return;
+
+	free(s->aia);
+	free(s->aki);
+	free(s->sia);
+	free(s->ski);
+	free(s->pfxs);
+	free(s);
+}
+
+/*
+ * Serialise parsed SPL content.
+ * See spl_read() for reader.
+ */
+void
+spl_buffer(struct ibuf *b, const struct spl *s)
+{
+	io_simple_buffer(b, &s->valid, sizeof(s->valid));
+	io_simple_buffer(b, &s->asid, sizeof(s->asid));
+	io_simple_buffer(b, &s->talid, sizeof(s->talid));
+	io_simple_buffer(b, &s->pfxsz, sizeof(s->pfxsz));
+	io_simple_buffer(b, &s->expires, sizeof(s->expires));
+
+	io_simple_buffer(b, s->pfxs, s->pfxsz * sizeof(s->pfxs[0]));
+
+	io_str_buffer(b, s->aia);
+	io_str_buffer(b, s->aki);
+	io_str_buffer(b, s->ski);
+}
+
+/*
+ * Read parsed SPL content from descriptor.
+ * See spl_buffer() for writer.
+ * Result must be passed to spl_free().
+ */
+struct spl *
+spl_read(struct ibuf *b)
+{
+	struct spl *s;
+
+	if ((s = calloc(1, sizeof(struct spl))) == NULL)
+		err(1, NULL);
+
+	io_read_buf(b, &s->valid, sizeof(s->valid));
+	io_read_buf(b, &s->asid, sizeof(s->asid));
+	io_read_buf(b, &s->talid, sizeof(s->talid));
+	io_read_buf(b, &s->pfxsz, sizeof(s->pfxsz));
+	io_read_buf(b, &s->expires, sizeof(s->expires));
+
+	if ((s->pfxs = calloc(s->pfxsz, sizeof(struct spl_pfx))) == NULL)
+		err(1, NULL);
+	io_read_buf(b, s->pfxs, s->pfxsz * sizeof(s->pfxs[0]));
+
+	io_read_str(b, &s->aia);
+	io_read_str(b, &s->aki);
+	io_read_str(b, &s->ski);
+	assert(s->aia && s->aki && s->ski);
+
+	return s;
+}
+
+static int
+spl_pfx_cmp(const void *va, const void *vb)
+{
+	struct spl_pfx *a = (struct spl_pfx *)va;
+	struct spl_pfx *b = (struct spl_pfx *)vb;
+
+	if (a->afi > b->afi)
+		return 1;
+	if (a->afi < b->afi)
+		return -1;
+
+	return spl_addr_cmp(a->afi, &a->prefix, &b->prefix);
+}
+
+/*
+ * Add each prefix in the SPL into the VSP tree.
+ * Updates "vsps" to be the number of VSPs and "uniqs" to be the unique
+ * number of prefixes.
+ */
+void
+spl_insert_vsps(struct vsp_tree *tree, struct spl *spl, struct repo *rp)
+{
+	struct vsp	*vsp, *found;
+	size_t		 i;
+
+	if ((vsp = calloc(1, sizeof(*vsp))) == NULL)
+		err(1, NULL);
+
+	vsp->asid = spl->asid;
+	vsp->talid = spl->talid;
+	if (rp != NULL)
+		vsp->repoid = repo_id(rp);
+	else
+		vsp->repoid = 0;
+	vsp->expires = spl->expires;
+
+	if ((found = RB_INSERT(vsp_tree, tree, vsp)) != NULL) {
+		/* already exists */
+		if (found->expires < vsp->expires) {
+			/* adjust unique count */
+			repo_stat_inc(repo_byid(found->repoid),
+			    found->talid, RTYPE_SPL, STYPE_DEC_UNIQUE);
+			found->expires = vsp->expires;
+			found->talid = vsp->talid;
+			found->repoid = vsp->repoid;
+			repo_stat_inc(rp, vsp->talid, RTYPE_SPL,
+			    STYPE_UNIQUE);
+		}
+		free(vsp);
+		vsp = found;
+	} else
+		repo_stat_inc(rp, vsp->talid, RTYPE_SPL, STYPE_UNIQUE);
+	repo_stat_inc(rp, spl->talid, RTYPE_SPL, STYPE_TOTAL);
+
+	/* merge content of multiple SPLs */
+	vsp->prefixes = recallocarray(vsp->prefixes, vsp->prefixesz,
+	    vsp->prefixesz + spl->pfxsz, sizeof(struct spl_pfx));
+	if (vsp->prefixes == NULL)
+		err(1, NULL);
+	memmove(vsp->prefixes + vsp->prefixesz, spl->pfxs,
+	    spl->pfxsz * sizeof(struct spl_pfx));
+	vsp->prefixesz += spl->pfxsz;
+
+	/* sort into canonical form */
+	qsort(vsp->prefixes, vsp->prefixesz, sizeof(struct spl_pfx),
+	    spl_pfx_cmp);
+
+	/* deduplicate */
+	for (i = 0; i <= vsp->prefixesz; i++) {
+		if (spl_pfx_cmp(&vsp->prefixes[i], &vsp->prefixes[i + 1]) == 0) {
+			memmove(vsp->prefixes + i, vsp->prefixes + i + 1,
+			    (vsp->prefixesz - i) * sizeof(struct spl_pfx));
+			vsp->prefixesz--;
+		}
+	}	
+}
+
+/*
+ * Comparison function for the RB tree
+ */
+static inline int
+vspcmp(struct vsp *a, struct vsp *b)
+{
+	if (a->asid > b->asid)
+		return 1;
+	if (a->asid < b->asid)
+		return -1;
+
+	return 0;
+}
+
+RB_GENERATE(vsp_tree, vsp, entry, vspcmp);
Index: validate.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/validate.c,v
diff -u -p -r1.71 validate.c
--- validate.c	1 Feb 2024 15:11:38 -0000	1.71
+++ validate.c	20 Feb 2024 11:06:22 -0000
@@ -195,6 +195,21 @@ valid_roa(const char *fn, struct cert *c
 }
 
 /*
+ * Validate our SPL: check that the asID is contained.
+ * Returns 1 if valid, 0 otherwise.
+ */
+int
+valid_spl(const char *fn, struct cert *cert, struct spl *spl)
+{
+	if (as_check_covered(spl->asid, spl->asid, cert->as, cert->asz) > 0)
+		return 1;
+
+	warnx("%s: SPL: uncovered ASID: %u", fn, spl->asid);
+
+	return 0;
+}
+
+/*
  * Validate a file by verifying the SHA256 hash of that file.
  * The file to check is passed as a file descriptor.
  * Returns 1 if hash matched, 0 otherwise. Closes fd when done.
Index: x509.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/x509.c,v
diff -u -p -r1.80 x509.c
--- x509.c	16 Feb 2024 05:18:29 -0000	1.80
+++ x509.c	20 Feb 2024 11:06:22 -0000
@@ -44,6 +44,7 @@ ASN1_OBJECT	*rsc_oid;	/* id-ct-signedChe
 ASN1_OBJECT	*aspa_oid;	/* id-ct-ASPA */
 ASN1_OBJECT	*tak_oid;	/* id-ct-SignedTAL */
 ASN1_OBJECT	*geofeed_oid;	/* id-ct-geofeedCSVwithCRLF */
+ASN1_OBJECT	*spl_oid;	/* id-ct-signedPrefixList */
 
 static const struct {
 	const char	 *oid;
@@ -116,6 +117,10 @@ static const struct {
 	{
 		.oid = "1.2.840.113549.1.9.16.1.50",
 		.ptr = &tak_oid,
+	},
+	{
+		.oid = "1.2.840.113549.1.9.16.1.51",
+		.ptr = &spl_oid,
 	},
 };