Index | Thread | Search

From:
Job Snijders <job@bsd.nl>
Subject:
rpki-client: implement erik object decoders & printers
To:
tech@openbsd.org
Date:
Sun, 16 Aug 2026 13:38:54 +0000

Download raw body.

Thread
  • Job Snijders:

    rpki-client: implement erik object decoders & printers

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 <job@bsd.nl>
+ *
+ * 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,
 	},
 };