rpki-client: implement erik object decoders & printers

Job Snijders <[email protected]>
Newsgroups gmane.os.openbsd.tech
Message-ID <[email protected]>
Dear all,

This changeset implements DER decoders & printers for the data
structures specified in draft-ietf-sidrops-rpki-erik-protocol-07 with
the IANA codepoints.

Example objects from a live relay:

ErikIndex: https://miso.sobornost.net/.well-known/erik/index/rpki.ripe.net
ErikSegmentIndex: https://miso.sobornost.net/.well-known/erik/segmentindex/rpki.ripe.net
ErikPartition: https://miso.sobornost.net/.well-known/ni/sha-256/zWDTqZw7ABhCtFYKr5RWodoIiUgxGLdvw2JBNf7dS5M

While we still are some way off from a native syncing client
implementation, having these printers as debugging & inspection utility
will help as a step towards that.

OK? Feedback?

Kind regards,

Job

Index: Makefile
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/Makefile,v
diff -u -p -r1.41 Makefile
--- Makefile	22 Jun 2026 21:25:44 -0000	1.41
+++ Makefile	16 Aug 2026 13:33:34 -0000
@@ -11,6 +11,7 @@ SRCS+=	cms.c
 SRCS+=	constraints.c
 SRCS+=	crl.c
 SRCS+=	encoding.c
+SRCS+=	erik.c
 SRCS+=	filemode.c
 SRCS+=	http.c
 SRCS+=	io.c
Index: erik.c
===================================================================
RCS file: erik.c
diff -N erik.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ erik.c	16 Aug 2026 13:33:34 -0000
@@ -0,0 +1,542 @@
+/*	$OpenBSD$ */
+/*
+ * Copyright (c) 2026 Job Snijders <[email protected]>
+ *
+ * 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 <err.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 "extern.h"
+#include "rpki-asn1.h"
+
+/*
+ * Erik obj definitions in draft-ietf-sidrops-rpki-erik-protocol-07, section 3.
+ */
+
+ASN1_ITEM_EXP EInd_ContentInfo_it;
+ASN1_ITEM_EXP ErikIndex_it;
+ASN1_ITEM_EXP PartitionRef_it;
+ASN1_ITEM_EXP EPar_ContentInfo_it;
+ASN1_ITEM_EXP ErikPartition_it;
+ASN1_ITEM_EXP ManifestRef_it;
+ASN1_ITEM_EXP ESI_ContentInfo_it;
+ASN1_ITEM_EXP ErikSegmentIndex_it;
+ASN1_ITEM_EXP SegmentRef_it;
+
+ASN1_SEQUENCE(EInd_ContentInfo) = {
+	ASN1_SIMPLE(EInd_ContentInfo, contentType, ASN1_OBJECT),
+	ASN1_EXP(EInd_ContentInfo, content, ErikIndex, 0),
+} ASN1_SEQUENCE_END(EInd_ContentInfo);
+
+IMPLEMENT_ASN1_FUNCTIONS(EInd_ContentInfo);
+
+ASN1_SEQUENCE(ErikIndex) = {
+	ASN1_EXP_OPT(ErikIndex, version, ASN1_INTEGER, 0),
+	ASN1_SIMPLE(ErikIndex, indexScope, ASN1_IA5STRING),
+	ASN1_SIMPLE(ErikIndex, indexTime, ASN1_GENERALIZEDTIME),
+	ASN1_SIMPLE(ErikIndex, hashAlg, X509_ALGOR),
+	ASN1_SEQUENCE_OF(ErikIndex, partitionList, PartitionRef),
+} ASN1_SEQUENCE_END(ErikIndex);
+
+IMPLEMENT_ASN1_FUNCTIONS(ErikIndex);
+
+ASN1_SEQUENCE(PartitionRef) = {
+	ASN1_SIMPLE(PartitionRef, hash, ASN1_OCTET_STRING),
+	ASN1_SIMPLE(PartitionRef, size, ASN1_INTEGER),
+} ASN1_SEQUENCE_END(PartitionRef);
+
+ASN1_SEQUENCE(EPar_ContentInfo) = {
+	ASN1_SIMPLE(EPar_ContentInfo, contentType, ASN1_OBJECT),
+	ASN1_EXP(EPar_ContentInfo, content, ErikPartition, 0),
+} ASN1_SEQUENCE_END(EPar_ContentInfo);
+
+IMPLEMENT_ASN1_FUNCTIONS(EPar_ContentInfo);
+
+ASN1_SEQUENCE(ErikPartition) = {
+	ASN1_EXP_OPT(ErikPartition, version, ASN1_INTEGER, 0),
+	ASN1_SIMPLE(ErikPartition, partitionTime, ASN1_GENERALIZEDTIME),
+	ASN1_SIMPLE(ErikPartition, hashAlg, X509_ALGOR),
+	ASN1_SEQUENCE_OF(ErikPartition, manifestList, ManifestRef),
+} ASN1_SEQUENCE_END(ErikPartition);
+
+IMPLEMENT_ASN1_FUNCTIONS(ErikPartition);
+
+ASN1_SEQUENCE(ManifestRef) = {
+	ASN1_SIMPLE(ManifestRef, hash, ASN1_OCTET_STRING),
+	ASN1_SIMPLE(ManifestRef, size, ASN1_INTEGER),
+	ASN1_SIMPLE(ManifestRef, aki, ASN1_OCTET_STRING),
+	ASN1_SIMPLE(ManifestRef, manifestNumber, ASN1_INTEGER),
+	ASN1_SIMPLE(ManifestRef, thisUpdate, ASN1_GENERALIZEDTIME),
+	ASN1_SEQUENCE_OF(ManifestRef, locations, ACCESS_DESCRIPTION),
+} ASN1_SEQUENCE_END(ManifestRef);
+
+ASN1_SEQUENCE(ESI_ContentInfo) = {
+	ASN1_SIMPLE(ESI_ContentInfo, contentType, ASN1_OBJECT),
+	ASN1_EXP(ESI_ContentInfo, content, ErikSegmentIndex, 0),
+} ASN1_SEQUENCE_END(ESI_ContentInfo);
+
+IMPLEMENT_ASN1_FUNCTIONS(ESI_ContentInfo);
+
+ASN1_SEQUENCE(ErikSegmentIndex) = {
+	ASN1_EXP_OPT(ErikSegmentIndex, version, ASN1_INTEGER, 0),
+	ASN1_SIMPLE(ErikSegmentIndex, segmentScope, ASN1_IA5STRING),
+	ASN1_SIMPLE(ErikSegmentIndex, segmentIndexTime, ASN1_GENERALIZEDTIME),
+	ASN1_SIMPLE(ErikSegmentIndex, hashAlg, X509_ALGOR),
+	ASN1_SEQUENCE_OF(ErikSegmentIndex, segmentList, SegmentRef),
+} ASN1_SEQUENCE_END(ErikSegmentIndex);
+
+IMPLEMENT_ASN1_FUNCTIONS(ErikSegmentIndex);
+
+ASN1_SEQUENCE(SegmentRef) = {
+	ASN1_SIMPLE(SegmentRef, segment, ASN1_GENERALIZEDTIME),
+	ASN1_SIMPLE(SegmentRef, index, ASN1_OCTET_STRING),
+} ASN1_SEQUENCE_END(SegmentRef);
+
+static int
+erik_parse_pref(const char *fn, struct eind *eind, const PartitionRef *pr)
+{
+	struct partref *partref;
+	const unsigned char *data;
+	int num, length, rc = 0;
+	uint64_t size = 0;
+
+	num = eind->parts_num++;
+	partref = &eind->parts[num];
+
+	data = ASN1_STRING_get0_data(pr->hash);
+	length = ASN1_STRING_length(pr->hash);
+	if (length != SHA256_DIGEST_LENGTH) {
+		warnx("%s: PartitionRef #%d corrupted: invalid hash", fn, num);
+		goto out;
+	}
+	memcpy(partref->hash, data, length);
+
+	if (!ASN1_INTEGER_get_uint64(&size, pr->size)) {
+		warnx("%s: PartitionRef #%d corrupted: size parse", fn, num);
+		goto out;
+	}
+	if (size < 100 || size > MAX_FILE_SIZE) {
+		warnx("%s: PartifionRef #%d corrupted: size", fn, num);
+		goto out;
+	}
+	partref->size = size;
+
+	rc = 1;
+ out:
+	return rc;
+}
+
+struct eind *
+erik_index_parse(const char *fn, const unsigned char *der, size_t len)
+{
+	const unsigned char *oder;
+	EInd_ContentInfo *ci = NULL;
+	ErikIndex *eind_asn1 = NULL;
+	const ASN1_OBJECT *oid;
+	struct eind *eind = NULL;
+	const unsigned char *data;
+	int i, length, nid, ptype, rc = 0;
+	const PartitionRef *pr;
+
+	if (der == NULL)
+		return NULL;
+
+	oder = der;
+	if ((ci = d2i_EInd_ContentInfo(NULL, &der, len)) == NULL) {
+		warnx("%s: d2i_EInd_ContentInfo", fn);
+		goto out;
+	}
+	if (der != oder + len) {
+		warnx("%s: %td bytes trailing garbage", fn, oder + len - der);
+		goto out;
+	}
+
+	if (OBJ_cmp(ci->contentType, eind_oid) != 0) {
+		char buf[128];
+
+		OBJ_obj2txt(buf, sizeof(buf), ci->contentType, 1);
+		warnx("%s: unexpected OID: got %s, want "
+		    "1.2.840.113549.1.9.16.1.55", fn, buf);
+		goto out;
+	}
+
+	eind_asn1 = ci->content;
+
+	if (!valid_econtent_version(fn, eind_asn1->version, 0))
+		goto out;
+
+	X509_ALGOR_get0(&oid, &ptype, NULL, eind_asn1->hashAlg);
+	if ((nid = OBJ_obj2nid(oid)) != NID_sha256 || ptype != V_ASN1_UNDEF) {
+		warnx("%s: hashAlg: want SHA256 object without parameters "
+		    "have %s with parameter type %d", fn, nid2str(nid), ptype);
+		goto out;
+	}
+
+	if ((eind = calloc(1, sizeof(*eind))) == NULL)
+		err(1, NULL);
+
+	data = ASN1_STRING_get0_data(eind_asn1->indexScope);
+	length = ASN1_STRING_length(eind_asn1->indexScope);
+
+	if (!valid_uri(data, length, NULL)) {
+		warnx("%s: invalid indexScope", fn);
+		goto out;
+	}
+
+	if ((eind->scope = strndup(data, length)) == NULL)
+		err(1, NULL);
+
+	if (!x509_get_generalized_time(fn, "ErikIndex indexTime",
+	    eind_asn1->indexTime, &eind->itime))
+		goto out;
+
+	if (sk_PartitionRef_num(eind_asn1->partitionList) <= 0) {
+		warnx("%s: corrupt partitionList", fn);
+		goto out;
+	}
+
+	if (sk_PartitionRef_num(eind_asn1->partitionList) > 256) {
+		warnx("%s: too many partitions in partitionList", fn);
+		goto out;
+	}
+
+	eind->parts = calloc(sk_PartitionRef_num(eind_asn1->partitionList),
+	    sizeof(eind->parts[0]));
+	if (eind->parts == NULL)
+		err(1, NULL);
+
+	for (i = 0; i < sk_PartitionRef_num(eind_asn1->partitionList); i++) {
+		pr = sk_PartitionRef_value(eind_asn1->partitionList, i);
+		if (!erik_parse_pref(fn, eind, pr))
+			goto out;
+		/* XXX: add uniqueness constraint? */
+	}
+
+	rc = 1;
+ out:
+	EInd_ContentInfo_free(ci);
+
+	if (rc == 0) {
+		eind_free(eind);
+		eind = NULL;
+	}
+
+	return eind;
+}
+
+static int
+erik_parse_mftref(const char *fn, struct epar *epar, const ManifestRef *mr)
+{
+	struct mftref *mftref;
+	const unsigned char *data;
+	int num, length, rc = 0;
+	uint64_t size = 0;
+	const ACCESS_DESCRIPTION *ad;
+
+	num = epar->mftrefs_num++;
+	mftref = &epar->mftrefs[num];
+
+	data = ASN1_STRING_get0_data(mr->hash);
+	length = ASN1_STRING_length(mr->hash);
+	if (length != SHA256_DIGEST_LENGTH) {
+		warnx("%s: ManifestRef #%d corrupted: invalid hash", fn, num);
+		goto out;
+	}
+	memcpy(mftref->hash, data, length);
+
+	if (!ASN1_INTEGER_get_uint64(&size, mr->size)) {
+		warnx("%s: ManifestRef #%d corrupted: size parse", fn, num);
+		goto out;
+	}
+	if (size < 1000 || size > MAX_FILE_SIZE) {
+		warnx("%s: ManifestRef #%d corrupted: size", fn, num);
+		goto out;
+	}
+	mftref->size = size;
+
+	data = ASN1_STRING_get0_data(mr->aki);
+	length = ASN1_STRING_length(mr->aki);
+	if (length != SHA_DIGEST_LENGTH) {
+		warnx("%s: ManifestRef #%d corrupted: invalid aki", fn, num);
+		goto out;
+	}
+	memcpy(mftref->aki, data, length);
+
+	mftref->seqnum = x509_convert_seqnum(fn, "ManifestRef seqnum",
+	    mr->manifestNumber);
+	if (mftref->seqnum == NULL)
+		goto out;
+
+	if (!x509_get_generalized_time(fn, "ManifestRef thisUpdate",
+	    mr->thisUpdate, &mftref->thisupdate))
+		goto out;
+
+	if (sk_ACCESS_DESCRIPTION_num(mr->locations) != 1) {
+		warnx("%s: ManifestRef #%d corrupted: unexpected number of "
+		    "locations", fn, num);
+		goto out;
+	}
+	ad = sk_ACCESS_DESCRIPTION_value(mr->locations, 0);
+	if (!x509_location(fn, "SIA: signedObject", ad->location, &mftref->sia))
+		goto out;
+
+	rc = 1;
+ out:
+	return rc;
+}
+
+struct epar *
+erik_part_parse(const char *fn, const unsigned char *der, size_t len)
+{
+	const unsigned char *oder;
+	EPar_ContentInfo *ci = NULL;
+	ErikPartition *epar_asn1 = NULL;
+	const ASN1_OBJECT *oid;
+	struct epar *epar = NULL;
+	int i, nid, ptype, rc = 0;
+	const ManifestRef *mr;
+
+	if (der == NULL)
+		return NULL;
+
+	oder = der;
+	if ((ci = d2i_EPar_ContentInfo(NULL, &der, len)) == NULL) {
+		warnx("%s: d2i_EPar_ContentInfo", fn);
+		goto out;
+	}
+	if (der != oder + len) {
+		warnx("%s: %td bytes trailing garbage", fn, oder + len - der);
+		goto out;
+	}
+
+	if (OBJ_cmp(ci->contentType, epar_oid) != 0) {
+		char buf[128];
+
+		OBJ_obj2txt(buf, sizeof(buf), ci->contentType, 1);
+		warnx("%s: unexpected OID: got %s, want "
+		    "1.2.840.113549.1.9.16.1.56", fn, buf);
+		goto out;
+	}
+
+	epar_asn1 = ci->content;
+
+	if (!valid_econtent_version(fn, epar_asn1->version, 0))
+		goto out;
+
+	X509_ALGOR_get0(&oid, &ptype, NULL, epar_asn1->hashAlg);
+	if ((nid = OBJ_obj2nid(oid)) != NID_sha256 || ptype != V_ASN1_UNDEF) {
+		warnx("%s: hashAlg: want SHA256 object without parameters "
+		    "have %s with parameter type %d", fn, nid2str(nid), ptype);
+		goto out;
+	}
+
+	if ((epar = calloc(1, sizeof(*epar))) == NULL)
+		err(1, NULL);
+
+	if (!x509_get_generalized_time(fn, "ErikPartition partitionTime",
+	    epar_asn1->partitionTime, &epar->ptime))
+		goto out;
+
+	if (sk_ManifestRef_num(epar_asn1->manifestList) <= 0) {
+		warnx("%s: corrupt manifestList", fn);
+		goto out;
+	}
+
+	epar->mftrefs = calloc(sk_ManifestRef_num(epar_asn1->manifestList),
+	    sizeof(epar->mftrefs[0]));
+	if (epar->mftrefs == NULL)
+		err(1, NULL);
+
+	for (i = 0; i < sk_ManifestRef_num(epar_asn1->manifestList); i++) {
+		mr = sk_ManifestRef_value(epar_asn1->manifestList, i);
+		if (!erik_parse_mftref(fn, epar, mr))
+			goto out;
+		/* XXX: add uniqueness constraint? */
+	}
+
+	rc = 1;
+ out:
+	EPar_ContentInfo_free(ci);
+
+	if (rc == 0) {
+		epar_free(epar);
+		epar = NULL;
+	}
+
+	return epar;
+}
+
+static int
+erik_parse_segref(const char *fn, struct esi *esi, const SegmentRef *sr)
+{
+	struct segref *segref;
+	const unsigned char *data;
+	int num, length, rc = 0;
+
+	num = esi->segrefs_num++;
+	segref = &esi->segrefs[num];
+
+	if (!x509_get_generalized_time(fn, "SegmentRef segment",
+	    sr->segment, &segref->segment))
+		goto out;
+
+	data = ASN1_STRING_get0_data(sr->index);
+	length = ASN1_STRING_length(sr->index);
+	if (length != SHA256_DIGEST_LENGTH) {
+		warnx("%s: SegmentRef #%d corrupted: invalid index", fn, num);
+		goto out;
+	}
+	memcpy(segref->index, data, length);
+
+	rc = 1;
+ out:
+	return rc;
+}
+
+struct esi *
+erik_segindex_parse(const char *fn, const unsigned char *der, size_t len)
+{
+	const unsigned char *oder;
+	ESI_ContentInfo *ci = NULL;
+	ErikSegmentIndex *esi_asn1 = NULL;
+	const ASN1_OBJECT *oid;
+	struct esi *esi = NULL;
+	const unsigned char *data;
+	int i, length, nid, ptype, rc = 0;
+	const SegmentRef *sr;
+
+	if (der == NULL)
+		return NULL;
+
+	oder = der;
+	if ((ci = d2i_ESI_ContentInfo(NULL, &der, len)) == NULL) {
+		warnx("%s: d2i_ESI_ContentInfo", fn);
+		goto out;
+	}
+	if (der != oder + len) {
+		warnx("%s: %td bytes trailing garbage", fn, oder + len - der);
+		goto out;
+	}
+
+	if (OBJ_cmp(ci->contentType, esi_oid) != 0) {
+		char buf[128];
+
+		OBJ_obj2txt(buf, sizeof(buf), ci->contentType, 1);
+		warnx("%s: unexpected OID: got %s, want "
+		    "1.2.840.113549.1.9.16.1.59", fn, buf);
+		goto out;
+	}
+
+	esi_asn1 = ci->content;
+
+	if (!valid_econtent_version(fn, esi_asn1->version, 0))
+		goto out;
+
+	X509_ALGOR_get0(&oid, &ptype, NULL, esi_asn1->hashAlg);
+	if ((nid = OBJ_obj2nid(oid)) != NID_sha256 || ptype != V_ASN1_UNDEF) {
+		warnx("%s: hashAlg: want SHA256 object without parameters "
+		    "have %s with parameter type %d", fn, nid2str(nid), ptype);
+		goto out;
+	}
+
+	if ((esi = calloc(1, sizeof(*esi))) == NULL)
+		err(1, NULL);
+
+	data = ASN1_STRING_get0_data(esi_asn1->segmentScope);
+	length = ASN1_STRING_length(esi_asn1->segmentScope);
+	if (!valid_uri(data, length, NULL)) {
+		warnx("%s: invalid segmentScope", fn);
+		goto out;
+	}
+	if ((esi->scope = strndup(data, length)) == NULL)
+		err(1, NULL);
+
+	if (!x509_get_generalized_time(fn, "ErikSegmentIndex segmentIndexTime",
+	    esi_asn1->segmentIndexTime, &esi->stime))
+		goto out;
+
+	if (sk_SegmentRef_num(esi_asn1->segmentList) <= 0) {
+		warnx("%s: corrupt segmentList", fn);
+		goto out;
+	}
+
+	esi->segrefs = calloc(sk_SegmentRef_num(esi_asn1->segmentList),
+	    sizeof(esi->segrefs[0]));
+	if (esi->segrefs == NULL)
+		err(1, NULL);
+
+	for (i = 0; i < sk_SegmentRef_num(esi_asn1->segmentList); i++) {
+		sr = sk_SegmentRef_value(esi_asn1->segmentList, i);
+		if (!erik_parse_segref(fn, esi, sr))
+			goto out;
+		/* XXX: add uniqueness constraint? */
+	}
+
+	rc = 1;
+ out:
+	ESI_ContentInfo_free(ci);
+
+	if (rc == 0) {
+		esi_free(esi);
+		esi = NULL;
+	}
+
+	return esi;
+}
+
+void
+eind_free(struct eind *eind)
+{
+	if (eind == NULL)
+		return;
+
+	free(eind->parts);
+	free(eind);
+}
+
+void
+epar_free(struct epar *epar)
+{
+	size_t i;
+
+	if (epar == NULL)
+		return;
+
+	for (i = 0; i < epar->mftrefs_num; i++) {
+		free(epar->mftrefs[i].seqnum);
+		free(epar->mftrefs[i].sia);
+	}
+
+	free(epar->mftrefs);
+	free(epar);
+}
+
+void
+esi_free(struct esi *esi)
+{
+	if (esi == NULL)
+		return;
+
+	free(esi->segrefs);
+	free(esi);
+}
Index: extern.h
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/extern.h,v
diff -u -p -r1.290 extern.h
--- extern.h	15 Jul 2026 07:53:06 -0000	1.290
+++ extern.h	16 Aug 2026 13:33:35 -0000
@@ -234,6 +234,9 @@ enum rtype {
 	RTYPE_SPL,
 	RTYPE_CCR,
 	RTYPE_GZ,
+	RTYPE_EIND,
+	RTYPE_EPAR,
+	RTYPE_ESI,
 };
 
 enum location {
@@ -290,6 +293,48 @@ struct mft {
 };
 
 /*
+ * Internal datastructures for Erik objects
+ */
+struct partref {
+	unsigned char	 hash[SHA256_DIGEST_LENGTH];
+	size_t		 size;
+};
+
+struct eind {
+	char		*scope;
+	time_t		 itime;
+	size_t		 parts_num;
+	struct partref	*parts;
+};
+
+struct mftref {
+	unsigned char	 hash[SHA256_DIGEST_LENGTH];
+	size_t		 size;
+	unsigned char	 aki[SHA_DIGEST_LENGTH];
+	char		*seqnum;
+	time_t		 thisupdate;
+	char		*sia;
+};
+
+struct epar {
+	time_t		 ptime;
+	size_t		 mftrefs_num;
+	struct mftref	*mftrefs;
+};
+
+struct segref {
+	time_t		 segment;
+	unsigned char	 index[SHA256_DIGEST_LENGTH];
+};
+
+struct esi {
+	char		*scope;
+	time_t		 stime;
+	size_t		 segrefs_num;
+	struct segref	*segrefs;
+};
+
+/*
  * An IP address prefix for a given ROA.
  * This encodes the maximum length, AFI (v6/v4), and address.
  * FIXME: are the min/max necessary or just used in one place?
@@ -732,6 +777,9 @@ extern ASN1_OBJECT *aspa_oid;
 extern ASN1_OBJECT *tak_oid;
 extern ASN1_OBJECT *spl_oid;
 extern ASN1_OBJECT *ccr_oid;
+extern ASN1_OBJECT *eind_oid;
+extern ASN1_OBJECT *epar_oid;
+extern ASN1_OBJECT *esi_oid;
 
 extern int verbose;
 extern int noop;
@@ -1066,6 +1114,19 @@ void ccr_insert_roa(struct ccr_vrp_tree 
 void ccr_insert_tas(struct ccr_tas_tree *, const struct cert *);
 void ccr_insert_mft_sub(struct ccr_mft_tree *, const struct cert *);
 void serialize_ccr_content(struct validation_data *);
+
+/*
+ * Erik Synchronisation protocol objects
+ */
+struct eind *erik_index_parse(const char *, const unsigned char *, size_t);
+struct epar *erik_part_parse(const char *, const unsigned char *, size_t);
+struct esi *erik_segindex_parse(const char *, const unsigned char *, size_t);
+void eind_free(struct eind *);
+void epar_free(struct epar *);
+void esi_free(struct esi *);
+void eind_print(struct eind *);
+void epar_print(struct epar *);
+void esi_print(struct esi *);
 
 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.86 filemode.c
--- filemode.c	9 Jul 2026 11:39:19 -0000	1.86
+++ filemode.c	16 Aug 2026 13:33:35 -0000
@@ -351,6 +351,18 @@ rtype_from_der(const char *fn, const uns
 				rtype = RTYPE_CCR;
 				goto out;
 			}
+			if (OBJ_cmp(obj, eind_oid) == 0) {
+				rtype = RTYPE_EIND;
+				goto out;
+			}
+			if (OBJ_cmp(obj, epar_oid) == 0) {
+				rtype = RTYPE_EPAR;
+				goto out;
+			}
+			if (OBJ_cmp(obj, esi_oid) == 0) {
+				rtype = RTYPE_ESI;
+				goto out;
+			}
 		}
 
 		if (CMS_get0_SignerInfos(cms) == NULL) {
@@ -420,6 +432,9 @@ proc_parser_file(char *file, unsigned ch
 	struct cert *cert = NULL;
 	struct ccr *ccr = NULL;
 	struct crl *crl = NULL;
+	struct eind *eind = NULL;
+	struct epar *epar = NULL;
+	struct esi *esi = NULL;
 	struct mft *mft = NULL;
 	struct roa *roa = NULL;
 	struct rsc *rsc = NULL;
@@ -530,6 +545,24 @@ proc_parser_file(char *file, unsigned ch
 			break;
 		crl_print(crl);
 		break;
+	case RTYPE_EIND:
+		eind = erik_index_parse(file, buf, len);
+		if (eind == NULL)
+			break;
+		eind_print(eind);
+		break;
+	case RTYPE_EPAR:
+		epar = erik_part_parse(file, buf, len);
+		if (epar == NULL)
+			break;
+		epar_print(epar);
+		break;
+	case RTYPE_ESI:
+		esi = erik_segindex_parse(file, buf, len);
+		if (esi == NULL)
+			break;
+		esi_print(esi);
+		break;
 	case RTYPE_MFT:
 		mft = mft_parse(&cert, file, -1, buf, len);
 		if (mft == NULL)
@@ -730,6 +763,9 @@ proc_parser_file(char *file, unsigned ch
 	cert_free(cert);
 	ccr_free(ccr);
 	crl_free(crl);
+	eind_free(eind);
+	epar_free(epar);
+	esi_free(esi);
 	mft_free(mft);
 	roa_free(roa);
 	rsc_free(rsc);
Index: print.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/print.c,v
diff -u -p -r1.79 print.c
--- print.c	7 Jul 2026 13:38:54 -0000	1.79
+++ print.c	16 Aug 2026 13:33:35 -0000
@@ -1092,3 +1092,135 @@ ccr_print(struct ccr *ccr)
 	if (ccr->brks_hash != NULL)
 		print_ccr_rkstate(ccr);
 }
+
+void
+eind_print(struct eind *eind)
+{
+	size_t i;
+	char *hash;
+	struct partref *pr;
+
+	if (outformats & FORMAT_JSON) {
+		json_do_string("type", "erikindex");
+		json_do_int("indextime", eind->itime);
+		json_do_string("indexscope", eind->scope);
+	} else {
+		printf("Index time:               %s\n",
+		    time2str(eind->itime));
+		printf("Index scope:              %s\n", eind->scope);
+		printf("Partitions:               ");
+	}
+
+	if (outformats & FORMAT_JSON)
+		json_do_array("partitions");
+	for (i = 0; i < eind->parts_num; i++) {
+		pr = &eind->parts[i];
+
+		if (base64_encode(pr->hash, sizeof(pr->hash), &hash) == -1)
+			errx(1, "base64_encode failure");
+
+		if (outformats & FORMAT_JSON) {
+			json_do_object("partition", 1);
+			json_do_string("hash", hash);
+			json_do_uint("size", pr->size);
+			json_do_end();
+		} else {
+			if (i > 0)
+				printf("%26s", "");
+			printf("%zu: hash: %s (size: %zu)\n", i + 1, hash,
+			    pr->size);
+		}
+		free(hash);
+	}
+	if (outformats & FORMAT_JSON)
+		json_do_end();
+}
+
+void
+epar_print(struct epar *epar)
+{
+	size_t i;
+	struct mftref *mr;
+	char *aki, *hash;
+
+	if (outformats & FORMAT_JSON) {
+		json_do_string("type", "erikpart");
+		json_do_int("partitiontime", epar->ptime);
+	} else {
+		printf("Partition time:           %s\n",
+		    time2str(epar->ptime));
+		printf("Manifest references:      ");
+	}
+
+	if (outformats & FORMAT_JSON)
+		json_do_array("manifestrefs");
+	for (i = 0; i < epar->mftrefs_num; i++) {
+		mr = &epar->mftrefs[i];
+
+		aki = hex_encode(mr->aki, SHA_DIGEST_LENGTH);
+
+		if (base64_encode(mr->hash, sizeof(mr->hash), &hash) == -1)
+			errx(1, "base64_encode failure");
+
+		if (outformats & FORMAT_JSON) {
+			json_do_object("manifestrefs", 1);
+			json_do_string("hash", hash);
+			json_do_uint("size", mr->size);
+			json_do_string("aki", aki);
+			json_do_end();
+		} else {
+			if (i > 0)
+				printf("%26s", "");
+			printf("%zu: hash:%s size:%zu aki:%s seqnum:%s "
+			    "thisupdate:%lld sia:%s\n", i + 1, hash,
+			    mr->size, aki, mr->seqnum,
+			    (long long)mr->thisupdate, mr->sia);
+		}
+		free(aki);
+		free(hash);
+	}
+	if (outformats & FORMAT_JSON)
+		json_do_end();
+}
+
+void
+esi_print(struct esi *esi)
+{
+	size_t i;
+	struct segref *sr;
+	char *hash;
+
+	if (outformats & FORMAT_JSON) {
+		json_do_string("type", "eriksegmentindex");
+		json_do_int("segmentindextime", esi->stime);
+	} else {
+		printf("Segment index time:       %s\n",
+		    time2str(esi->stime));
+		printf("Segment references:       ");
+	}
+
+	if (outformats & FORMAT_JSON)
+		json_do_array("segmentrefs");
+	for (i = 0; i < esi->segrefs_num; i++) {
+		sr = &esi->segrefs[i];
+
+		if (base64_encode(sr->index, sizeof(sr->index), &hash) == -1)
+			errx(1, "base64_encode failure");
+
+		if (outformats & FORMAT_JSON) {
+			json_do_object("segmentrefs", 1);
+			json_do_uint("segment", sr->segment);
+			json_do_string("index", hash);
+			json_do_end();
+		} else {
+			if (i > 0)
+				printf("%26s", "");
+			printf("%zu: segment:%lld index:%s (%s)\n", i + 1,
+			    (long long)sr->segment, hash,
+			    time2str(sr->segment));
+		}
+		free(hash);
+	}
+	if (outformats & FORMAT_JSON)
+		json_do_end();
+}
Index: rpki-asn1.h
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/rpki-asn1.h,v
diff -u -p -r1.14 rpki-asn1.h
--- rpki-asn1.h	7 Apr 2026 11:52:21 -0000	1.14
+++ rpki-asn1.h	16 Aug 2026 13:33:35 -0000
@@ -235,6 +235,112 @@ DECLARE_ASN1_FUNCTIONS(ContentInfo);
 
 
 /*
+ * Erik Synchronisation Protocol objects
+ * reference: draft-ietf-sidrops-rpki-erik-protocol-07
+ */
+
+extern ASN1_ITEM_EXP EInd_ContentInfo_it;
+extern ASN1_ITEM_EXP EPar_ContentInfo_it;
+extern ASN1_ITEM_EXP ESI_ContentInfo_it;
+extern ASN1_ITEM_EXP ErikIndex_it;
+extern ASN1_ITEM_EXP ErikPartition_it;
+extern ASN1_ITEM_EXP ErikSegmentIndex_it;
+extern ASN1_ITEM_EXP ManifestRef_it;
+extern ASN1_ITEM_EXP PartitionRef_it;
+extern ASN1_ITEM_EXP SegmentRef_it;
+
+typedef struct {
+	ASN1_OCTET_STRING *hash;
+	ASN1_INTEGER *size;
+} PartitionRef;
+
+DECLARE_STACK_OF(PartitionRef);
+
+#ifndef DEFINE_STACK_OF
+#define sk_PartitionRef_num(sk)		SKM_sk_num(PartitionRef, (sk))
+#define sk_PartitionRef_value(sk, i)	SKM_sk_value(PartitionRef, (sk), (i))
+#endif
+
+typedef struct {
+	ASN1_INTEGER *version;
+	ASN1_IA5STRING *indexScope;
+	ASN1_GENERALIZEDTIME *indexTime;
+	X509_ALGOR *hashAlg;
+	STACK_OF(PartitionRef) *partitionList;
+} ErikIndex;
+
+DECLARE_ASN1_FUNCTIONS(ErikIndex);
+
+typedef struct {
+	ASN1_OBJECT *contentType;
+	ErikIndex *content;
+} EInd_ContentInfo;;
+
+DECLARE_ASN1_FUNCTIONS(EInd_ContentInfo);
+
+typedef struct {
+	ASN1_OCTET_STRING *hash;
+	ASN1_INTEGER *size;
+	ASN1_OCTET_STRING *aki;
+	ASN1_INTEGER *manifestNumber;
+	ASN1_GENERALIZEDTIME *thisUpdate;
+	STACK_OF(ACCESS_DESCRIPTION) *locations;
+} ManifestRef;
+
+DECLARE_STACK_OF(ManifestRef);
+
+#ifndef DEFINE_STACK_OF
+#define sk_ManifestRef_num(sk)		SKM_sk_num(ManifestRef, (sk))
+#define sk_ManifestRef_value(sk, i)	SKM_sk_value(ManifestRef, (sk), (i))
+#endif
+
+typedef struct {
+	ASN1_INTEGER *version;
+	ASN1_GENERALIZEDTIME *partitionTime;
+	X509_ALGOR *hashAlg;
+	STACK_OF(ManifestRef) *manifestList;
+} ErikPartition;
+
+DECLARE_ASN1_FUNCTIONS(ErikPartition);
+
+typedef struct {
+	ASN1_OBJECT *contentType;
+	ErikPartition *content;
+} EPar_ContentInfo;;
+
+DECLARE_ASN1_FUNCTIONS(EPar_ContentInfo);
+
+typedef struct {
+	ASN1_GENERALIZEDTIME *segment;
+	ASN1_OCTET_STRING *index;
+} SegmentRef;
+
+DECLARE_STACK_OF(SegmentRef);
+
+#ifndef DEFINE_STACK_OF
+#define sk_SegmentRef_num(sk)		SKM_sk_num(SegmentRef, (sk))
+#define sk_SegmentRef_value(sk, i)	SKM_sk_value(SegmentRef, (sk), (i))
+#endif
+
+typedef struct {
+	ASN1_INTEGER *version;
+	ASN1_IA5STRING *segmentScope;
+	ASN1_GENERALIZEDTIME *segmentIndexTime;
+	X509_ALGOR *hashAlg;
+	STACK_OF(SegmentRef) *segmentList;
+} ErikSegmentIndex;
+
+DECLARE_ASN1_FUNCTIONS(ErikSegmentIndex);
+
+typedef struct {
+	ASN1_OBJECT *contentType;
+	ErikSegmentIndex *content;
+} ESI_ContentInfo;;
+
+DECLARE_ASN1_FUNCTIONS(ESI_ContentInfo);
+
+
+/*
  * RPKI Manifest
  * reference: RFC 9286.
  */
Index: x509.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/x509.c,v
diff -u -p -r1.133 x509.c
--- x509.c	21 Jun 2026 19:23:56 -0000	1.133
+++ x509.c	16 Aug 2026 13:33:35 -0000
@@ -47,6 +47,9 @@ ASN1_OBJECT	*aspa_oid;	/* id-ct-ASPA */
 ASN1_OBJECT	*tak_oid;	/* id-ct-SignedTAL */
 ASN1_OBJECT	*spl_oid;	/* id-ct-signedPrefixList */
 ASN1_OBJECT	*ccr_oid;	/* id-ct-rpkiCanonicalCacheRepresentation */
+ASN1_OBJECT	*eind_oid;	/* id-ct-rpkiErikIndex */
+ASN1_OBJECT	*epar_oid;	/* id-ct-rpkiErikPartition */
+ASN1_OBJECT	*esi_oid;	/* id-ct-rpkiErikSegmentIndex */
 
 static const struct {
 	const char	 *oid;
@@ -119,6 +122,18 @@ static const struct {
 	{
 		.oid = "1.2.840.113549.1.9.16.1.54",
 		.ptr = &ccr_oid,
+	},
+	{
+		.oid = "1.2.840.113549.1.9.16.1.55",
+		.ptr = &eind_oid,
+	},
+	{
+		.oid = "1.2.840.113549.1.9.16.1.56",
+		.ptr = &epar_oid,
+	},
+	{
+		.oid = "1.2.840.113549.1.9.16.1.59",
+		.ptr = &esi_oid,
 	},
 };
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.