Download raw body.
rpki-client: Canonical Cache Representation
Dear all,
A follow-up based on some off-list review cycles with tb@ (thank you!)
A few notes:
1/ Despite various refactoring efforts, the generate_roapayloadstate()
function remains somewhat unsatisfactory in the sense that it seems a
quite complicated gearbox is required to populated the nested data
structure in ROA payloads.
2/ ASN1_ITEM_ref() is an undocumented libcrypto asn1.h macro.
3/ The same output infrastructure that writes out the other outputs
(json, openbgpd, ...) is used to carefully write out the rpki.ccr
file.
OK?
Kind regards,
Job
Index: Makefile
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/Makefile,v
diff -u -p -r1.36 Makefile
--- Makefile 14 Jul 2025 20:42:21 -0000 1.36
+++ Makefile 21 Aug 2025 21:08:50 -0000
@@ -4,6 +4,7 @@ PROG= rpki-client
SRCS+= as.c
SRCS+= aspa.c
+SRCS+= ccr.c
SRCS+= cert.c
SRCS+= cms.c
SRCS+= constraints.c
Index: ccr.c
===================================================================
RCS file: ccr.c
diff -N ccr.c
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ ccr.c 21 Aug 2025 21:08:51 -0000
@@ -0,0 +1,636 @@
+/* $OpenBSD$ */
+/*
+ * Copyright (c) 2025 Job Snijders <job@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/socket.h>
+#include <sys/tree.h>
+
+#include <assert.h>
+#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 "extern.h"
+#include "rpki-asn1.h"
+
+/*
+ * RpkiCanonicalCacheRepresentation-2025
+ * { iso(1) member-body(2) us(840) rsadsi(113549)
+ * pkcs(1) pkcs9(9) smime(16) mod(0) id-mod-rpkiCCR-2025(TBD) }
+ *
+ * DEFINITIONS EXPLICIT TAGS ::=
+ * BEGIN
+ *
+ * IMPORTS
+ * CONTENT-TYPE, Digest, DigestAlgorithmIdentifier
+ * FROM CryptographicMessageSyntax-2010 -- in [RFC6268]
+ * { iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
+ * pkcs-9(9) smime(16) modules(0) id-mod-cms-2009(58) };
+ *
+ * KeyIdentifier
+ * FROM PKIX1Implicit88 -- in [RFC5280]
+ * { iso(1) identified-organization(3) dod(6) internet(1) security(5)
+ * mechanisms(5) pkix(7) id-mod(0) id-pkix1-implicit(19) }
+ *
+ * -- in [draft-spaghetti-sidrops-rpki-erik-protocol-01]
+ * -- https://sobornost.net/~job/draft-spaghetti-sidrops-rpki-erik-protocol.html
+ * ManifestRef
+ * FROM RpkiErikPartition-2025
+ * { iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
+ * pkcs9(9) smime(16) mod(0) id-mod-rpkiErikPartition-2025(TBD) }
+ *
+ * ASID, ROAIPAddressFamily
+ * FROM RPKI-ROA-2023 -- in [RFC9582]
+ * { so(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
+ * pkcs9(9) smime(16) mod(0) id-mod-rpkiROA-2023(75) }
+ *
+ * ct-rpkiCanonicalCacheRepresentation CONTENT-TYPE ::=
+ * { TYPE RpkiCanonicalCacheRepresentation
+ * IDENTIFIED BY id-ct-rpkiCanonicalCacheRepresentation }
+ *
+ * id-ct-rpkiCanonicalCacheRepresentation OBJECT IDENTIFIER ::=
+ * { iso(1) identified-organization(3) dod(6) internet(1) private(4)
+ * enterprise(1) snijders(41948) ccr(825) }
+ *
+ * RpkiCanonicalCacheRepresentation ::= SEQUENCE {
+ * version [0] INTEGER DEFAULT 0,
+ * hashAlg DigestAlgorithmIdentifier,
+ * producedAt GeneralizedTime,
+ * mfts [1] ManifestState OPTIONAL,
+ * vrps [2] ROAPayloadState OPTIONAL,
+ * vaps [3] ASPAPayloadState OPTIONAL,
+ * ... }
+ * -- at least one of mfts or vrps MUST be present
+ * ( WITH COMPONENTS { ..., mfts PRESENT } |
+ * WITH COMPONENTS { ..., vrps PRESENT } |
+ * WITH COMPONENTS { ..., vaps PRESENT } )
+ *
+ * ManifestState ::= SEQUENCE {
+ * mftrefs SEQUENCE OF ManifestRef,
+ * mostRecentUpdate GeneralizedTime,
+ * hash Digest OPTIONAL }
+ *
+ * ROAPayloadState ::= SEQUENCE {
+ * rps SEQUENCE OF ROAPayloadSet,
+ * hash Digest OPTIONAL }
+ *
+ * ROAPayloadSet ::= SEQUENCE {
+ * asID ASID,
+ * ipAddrBlocks SEQUENCE (SIZE(1..2)) OF ROAIPAddressFamily }
+ *
+ * ASPAPayloadState ::= SEQUENCE {
+ * aps SEQUENCE OF ASPAPayloadSet,
+ * hash Digest OPTIONAL }
+ *
+ * ASPAPayloadSet ::= SEQUENCE {
+ * asID ASID
+ * providers SEQUENCE (SIZE(1..MAX)) OF ASID }
+ *
+ * END
+ */
+
+ASN1_ITEM_EXP ManifestRef_it;
+ASN1_ITEM_EXP ManifestRefs_it;
+ASN1_ITEM_EXP ROAPayloadSet_it;
+ASN1_ITEM_EXP ROAPayloadSets_it;
+ASN1_ITEM_EXP ASPAPayloadSet_it;
+ASN1_ITEM_EXP ASPAPayloadSets_it;
+ASN1_ITEM_EXP CanonicalCacheRepresentation_it;
+ASN1_ITEM_EXP CONTENT_TYPE_it;
+
+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_SEQUENCE_OF(ManifestRef, location, ACCESS_DESCRIPTION),
+} ASN1_SEQUENCE_END(ManifestRef);
+
+IMPLEMENT_ASN1_FUNCTIONS(ManifestRef);
+
+ASN1_ITEM_TEMPLATE(ManifestRefs) =
+ ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_SEQUENCE_OF, 0, mftrefs, ManifestRef)
+ASN1_ITEM_TEMPLATE_END(ManifestRefs)
+
+IMPLEMENT_ASN1_ENCODE_FUNCTIONS_fname(ManifestRefs, ManifestRefs, ManifestRefs);
+
+ASN1_SEQUENCE(ManifestState) = {
+ ASN1_SEQUENCE_OF(ManifestState, mftrefs, ManifestRef),
+ ASN1_SIMPLE(ManifestState, mostRecentUpdate, ASN1_GENERALIZEDTIME),
+ ASN1_OPT(ManifestState, hash, ASN1_OCTET_STRING),
+} ASN1_SEQUENCE_END(ManifestState);
+
+IMPLEMENT_ASN1_FUNCTIONS(ManifestState);
+
+ASN1_SEQUENCE(ROAPayloadSet) = {
+ ASN1_SIMPLE(ROAPayloadSet, asID, ASN1_INTEGER),
+ ASN1_SEQUENCE_OF(ROAPayloadSet, ipAddrBlocks, ROAIPAddressFamily),
+} ASN1_SEQUENCE_END(ROAPayloadSet);
+
+IMPLEMENT_ASN1_FUNCTIONS(ROAIPAddress);
+IMPLEMENT_ASN1_FUNCTIONS(ROAIPAddressFamily);
+IMPLEMENT_ASN1_FUNCTIONS(ROAPayloadSet);
+
+ASN1_ITEM_TEMPLATE(ROAPayloadSets) =
+ ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_SEQUENCE_OF, 0, rps, ROAPayloadSet)
+ASN1_ITEM_TEMPLATE_END(ROAPayloadSets)
+
+IMPLEMENT_ASN1_ENCODE_FUNCTIONS_fname(ROAPayloadSets, ROAPayloadSets,
+ ROAPayloadSets);
+
+ASN1_SEQUENCE(ROAPayloadState) = {
+ ASN1_SEQUENCE_OF(ROAPayloadState, rps, ROAPayloadSet),
+ ASN1_OPT(ROAPayloadState, hash, ASN1_OCTET_STRING),
+} ASN1_SEQUENCE_END(ROAPayloadState);
+
+IMPLEMENT_ASN1_FUNCTIONS(ROAPayloadState);
+
+ASN1_SEQUENCE(ASPAPayloadSet) = {
+ ASN1_SIMPLE(ASPAPayloadSet, asID, ASN1_INTEGER),
+ ASN1_SEQUENCE_OF(ASPAPayloadSet, providers, ASN1_INTEGER),
+} ASN1_SEQUENCE_END(ASPAPayloadSet);
+
+IMPLEMENT_ASN1_FUNCTIONS(ASPAPayloadSet);
+
+ASN1_ITEM_TEMPLATE(ASPAPayloadSets) =
+ ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_SEQUENCE_OF, 0, aps, ASPAPayloadSet)
+ASN1_ITEM_TEMPLATE_END(ASPAPayloadSets)
+
+IMPLEMENT_ASN1_ENCODE_FUNCTIONS_fname(ASPAPayloadSets, ASPAPayloadSets,
+ ASPAPayloadSets);
+
+ASN1_SEQUENCE(ASPAPayloadState) = {
+ ASN1_SEQUENCE_OF(ASPAPayloadState, aps, ASPAPayloadSet),
+ ASN1_OPT(ASPAPayloadState, hash, ASN1_OCTET_STRING),
+} ASN1_SEQUENCE_END(ASPAPayloadState);
+
+IMPLEMENT_ASN1_FUNCTIONS(ASPAPayloadState);
+
+ASN1_SEQUENCE(CanonicalCacheRepresentation) = {
+ ASN1_EXP_OPT(CanonicalCacheRepresentation, version, ASN1_INTEGER, 0),
+ ASN1_SIMPLE(CanonicalCacheRepresentation, hashAlg, ASN1_OBJECT),
+ ASN1_SIMPLE(CanonicalCacheRepresentation, producedAt,
+ ASN1_GENERALIZEDTIME),
+ ASN1_EXP_OPT(CanonicalCacheRepresentation, mfts, ManifestState, 1),
+ ASN1_EXP_OPT(CanonicalCacheRepresentation, vrps, ROAPayloadState, 2),
+ ASN1_EXP_OPT(CanonicalCacheRepresentation, vaps, ASPAPayloadState, 3),
+} ASN1_SEQUENCE_END(CanonicalCacheRepresentation);
+
+IMPLEMENT_ASN1_FUNCTIONS(CanonicalCacheRepresentation);
+
+ASN1_SEQUENCE(CONTENT_TYPE) = {
+ ASN1_SIMPLE(CONTENT_TYPE, oid, ASN1_OBJECT),
+ ASN1_EXP(CONTENT_TYPE, content, ASN1_OCTET_STRING, 0),
+} ASN1_SEQUENCE_END(CONTENT_TYPE);
+
+IMPLEMENT_ASN1_FUNCTIONS(CONTENT_TYPE);
+
+
+static void
+asn1int_set_seqnum(ASN1_INTEGER *aint, const char *seqnum)
+{
+ BIGNUM *bn = NULL;
+
+ if (!BN_hex2bn(&bn, seqnum))
+ errx(1, "BN_hex2bn");
+
+ if (BN_to_ASN1_INTEGER(bn, aint) == NULL)
+ errx(1, "BN_to_ASN1_INTEGER");
+
+ BN_free(bn);
+}
+
+static void
+location_add_sia(STACK_OF(ACCESS_DESCRIPTION) *sad, const char *sia)
+{
+ ACCESS_DESCRIPTION *ad = NULL;
+
+ if ((ad = ACCESS_DESCRIPTION_new()) == NULL)
+ errx(1, "ACCESS_DESCRIPTION_new");
+
+ ASN1_OBJECT_free(ad->method);
+ if ((ad->method = OBJ_nid2obj(NID_signedObject)) == NULL)
+ errx(1, "OBJ_nid2obj");
+
+ GENERAL_NAME_free(ad->location);
+ ad->location = a2i_GENERAL_NAME(NULL, NULL, NULL, GEN_URI, sia, 0);
+ if (ad->location == NULL)
+ errx(1, "a2i_GENERAL_NAME");
+
+ if (sk_ACCESS_DESCRIPTION_push(sad, ad) <= 0)
+ errx(1, "sk_ACCESS_DESCRIPTION_push");
+}
+
+static void
+append_cached_manifest(STACK_OF(ManifestRef) *mftrefs, struct ccr_mft *cm)
+{
+ ManifestRef *mftref;
+
+ if ((mftref = ManifestRef_new()) == NULL)
+ errx(1, "ManifestRef_new");
+
+ if (!ASN1_OCTET_STRING_set(mftref->hash, cm->hash, sizeof(cm->hash)))
+ errx(1, "ASN1_OCTET_STRING_set");
+
+ if (!ASN1_OCTET_STRING_set(mftref->aki, cm->aki, sizeof(cm->aki)))
+ errx(1, "ASN1_STRING_set");
+
+ if (!ASN1_INTEGER_set_uint64(mftref->size, cm->size))
+ errx(1, "ASN1_INTEGER_set_uint64");
+
+ asn1int_set_seqnum(mftref->manifestNumber, cm->seqnum);
+
+ location_add_sia(mftref->location, cm->sia);
+
+ if (sk_ManifestRef_push(mftrefs, mftref) <= 0)
+ errx(1, "sk_ManifestRef_push");
+}
+
+static ASN1_STRING *
+hash_asn1_item(const ASN1_ITEM *it, void *val)
+{
+ unsigned char hash[SHA256_DIGEST_LENGTH];
+ ASN1_STRING *astr;
+
+ if (!ASN1_item_digest(it, EVP_sha256(), val, hash, NULL))
+ errx(1, "ASN1_item_digest");
+
+ if ((astr = ASN1_STRING_new()) == NULL)
+ errx(1, "ASN1_STRING_new");
+
+ if (!ASN1_OCTET_STRING_set(astr, hash, sizeof(hash)))
+ errx(1, "ASN1_STRING_set");
+
+ return astr;
+}
+
+static ManifestState *
+generate_manifeststate(struct validation_data *vd)
+{
+ struct ccr *ccr = &vd->ccr;
+ ManifestState *ms;
+ struct ccr_mft *cm;
+ time_t most_recent_update = 0;
+
+ if ((ms = ManifestState_new()) == NULL)
+ errx(1, "ManifestState_new");
+
+ RB_FOREACH(cm, ccr_mft_tree, &ccr->mfts) {
+ append_cached_manifest(ms->mftrefs, cm);
+
+ if (cm->thisupdate > most_recent_update)
+ most_recent_update = cm->thisupdate;
+ }
+
+ if (ASN1_GENERALIZEDTIME_set(ms->mostRecentUpdate,
+ most_recent_update) == NULL)
+ errx(1, "ASN1_GENERALIZEDTIME_set");
+
+ ms->hash = hash_asn1_item(ASN1_ITEM_ref(ManifestRefs), ms->mftrefs);
+
+ if (base64_encode(ms->hash->data, ms->hash->length, &ccr->mfts_hash)
+ == -1)
+ errx(1, "base64_encode");
+
+ return ms;
+}
+
+static void
+append_cached_vrp(STACK_OF(ROAIPAddress) *addresses, struct vrp *vrp)
+{
+ ROAIPAddress *ripa;
+ ASN1_BIT_STRING *addr;
+ int num_bits, num_bytes;
+ uint8_t unused_bits;
+
+ if ((ripa = ROAIPAddress_new()) == NULL)
+ errx(1, "ROAIPAddress_new");
+
+ if ((addr = ASN1_BIT_STRING_new()) == NULL)
+ errx(1, "ASN1_BIT_STRING_new");
+
+ num_bytes = (vrp->addr.prefixlen + 7) / 8;
+ num_bits = vrp->addr.prefixlen % 8;
+
+ unused_bits = 0;
+ if (num_bits > 0)
+ unused_bits = 8 - num_bits;
+
+ if (!ASN1_BIT_STRING_set(addr, vrp->addr.addr, num_bytes))
+ errx(1, "ASN1_BIT_STRING_set");
+
+ /* ip_addr_parse() handles unused bits, no need to clear them here. */
+ addr->flags |= ASN1_STRING_FLAG_BITS_LEFT | unused_bits;
+
+ ripa->address = addr;
+
+ if (vrp->maxlength > vrp->addr.prefixlen) {
+ if ((ripa->maxLength = ASN1_INTEGER_new()) == NULL)
+ errx(1, "ASN1_INTEGER_new");
+
+ if (!ASN1_INTEGER_set_uint64(ripa->maxLength, vrp->maxlength))
+ errx(1, "ASN1_INTEGER_set_uint64");
+ }
+
+ if (sk_ROAIPAddress_push(addresses, ripa) <= 0)
+ errx(1, "sk_ROAIPAddress_push");
+}
+
+static ROAPayloadState *
+generate_roapayloadstate(struct validation_data *vd)
+{
+ ROAPayloadState *vrps;
+ struct vrp *prev, *vrp;
+ ROAPayloadSet *rp;
+ ROAIPAddressFamily *ripaf;
+ unsigned char afibuf[2];
+
+ if ((vrps = ROAPayloadState_new()) == NULL)
+ errx(1, "ROAPayloadState_new");
+
+ prev = NULL;
+ RB_FOREACH(vrp, ccr_vrp_tree, &vd->ccr.vrps) {
+ if (prev == NULL || prev->asid != vrp->asid) {
+ if ((rp = ROAPayloadSet_new()) == NULL)
+ errx(1, "ROAPayloadSet_new");
+
+ if (!ASN1_INTEGER_set_uint64(rp->asID, vrp->asid))
+ errx(1, "ASN1_INTEGER_set_uint64");
+
+ if (sk_ROAPayloadSet_push(vrps->rps, rp) <= 0)
+ errx(1, "sk_ROAPayloadSet_push");
+ }
+
+ if (prev == NULL || prev->asid != vrp->asid ||
+ prev->afi != vrp->afi) {
+ if ((ripaf = ROAIPAddressFamily_new()) == NULL)
+ errx(1, "ROAIPAddressFamily_new");
+
+ /* vrp->afi is 1 or 2. */
+ afibuf[0] = 0;
+ afibuf[1] = vrp->afi;
+ if (!ASN1_OCTET_STRING_set(ripaf->addressFamily, afibuf,
+ sizeof(afibuf)))
+ errx(1, "ASN1_OCTET_STRING_set");
+
+ if (sk_ROAIPAddressFamily_push(rp->ipAddrBlocks,
+ ripaf) <= 0)
+ errx(1, "sk_ROAIPAddressFamily_push");
+ }
+
+ append_cached_vrp(ripaf->addresses, vrp);
+ prev = vrp;
+ }
+
+ vrps->hash = hash_asn1_item(ASN1_ITEM_ref(ROAPayloadSets), vrps->rps);
+
+ if (base64_encode(vrps->hash->data, vrps->hash->length,
+ &vd->ccr.vrps_hash) == -1)
+ errx(1, "base64_encode");
+
+ return vrps;
+}
+
+static void
+append_cached_aspa(STACK_OF(ASPAPayloadSet) *aps, struct vap *vap)
+{
+ ASPAPayloadSet *ap;
+ size_t i;
+
+ if ((ap = ASPAPayloadSet_new()) == NULL)
+ errx(1, "ASPAPayloadSet_new");
+
+ if (!ASN1_INTEGER_set_uint64(ap->asID, vap->custasid))
+ errx(1, "ASN1_INTEGER_set_uint64");
+
+ for (i = 0; i < vap->num_providers; i++) {
+ ASN1_INTEGER *ai;
+
+ if ((ai = ASN1_INTEGER_new()) == NULL)
+ errx(1, "ASN1_INTEGER");
+
+ if (!ASN1_INTEGER_set_uint64(ai, vap->providers[i]))
+ errx(1, "ASN1_INTEGER_set_uint64");
+
+ if ((sk_ASN1_INTEGER_push(ap->providers, ai)) <= 0)
+ errx(1, "sk_ASN1_INTEGER_push");
+ }
+
+ if ((sk_ASPAPayloadSet_push(aps, ap)) <= 0)
+ errx(1, "sk_ASPAPayloadSet_push");
+}
+
+static ASPAPayloadState *
+generate_aspapayloadstate(struct validation_data *vd)
+{
+ ASPAPayloadState *vaps;
+ struct vap *vap;
+
+ if ((vaps = ASPAPayloadState_new()) == NULL)
+ errx(1, "ASPAPayloadState_new");
+
+ RB_FOREACH(vap, vap_tree, &vd->vaps) {
+ append_cached_aspa(vaps->aps, vap);
+ }
+
+ vaps->hash = hash_asn1_item(ASN1_ITEM_ref(ASPAPayloadSets), vaps->aps);
+
+ if (base64_encode(vaps->hash->data, vaps->hash->length,
+ &vd->ccr.vaps_hash) == -1)
+ errx(1, "base64_encode");
+
+ return vaps;
+}
+
+static CanonicalCacheRepresentation *
+build_ccr(struct validation_data *vd)
+{
+ CanonicalCacheRepresentation *ccr = NULL;
+ time_t now = get_current_time();
+
+ if ((ccr = CanonicalCacheRepresentation_new()) == NULL)
+ errx(1, "CanonicalCacheRepresentation_new");
+
+ ASN1_OBJECT_free(ccr->hashAlg);
+ if ((ccr->hashAlg = OBJ_nid2obj(NID_sha256)) == NULL)
+ errx(1, "OBJ_nid2obj");
+
+ if (ASN1_GENERALIZEDTIME_set(ccr->producedAt, now) == NULL)
+ errx(1, "ASN1_GENERALIZEDTIME_set");
+
+ if ((ccr->mfts = generate_manifeststate(vd)) == NULL)
+ errx(1, "generate_manifeststate");
+
+ if ((ccr->vrps = generate_roapayloadstate(vd)) == NULL)
+ errx(1, "generate_roapayloadstate");
+
+ if ((ccr->vaps = generate_aspapayloadstate(vd)) == NULL)
+ errx(1, "generate_aspapayloadstate");
+
+ return ccr;
+}
+
+void
+generate_ccr_content(struct validation_data *vd)
+{
+ CanonicalCacheRepresentation *ccr;
+ CONTENT_TYPE *ct = NULL;
+ unsigned char *out = NULL;
+ int out_len;
+
+ ccr = build_ccr(vd);
+
+ if ((out_len = i2d_CanonicalCacheRepresentation(ccr, &out)) <= 0)
+ err(1, "i2d_CanonicalCacheRepresentation");
+
+ CanonicalCacheRepresentation_free(ccr);
+
+ if ((ct = CONTENT_TYPE_new()) == NULL)
+ errx(1, "CONTENT_TYPE_new");
+
+ /*
+ * At some point the below PEN OID should be replaced by one from IANA.
+ */
+ ASN1_OBJECT_free(ct->oid);
+ if ((ct->oid = OBJ_txt2obj("1.3.6.1.4.1.41948.825", 1)) == NULL)
+ errx(1, "OBJ_txt2obj");
+
+ if (!ASN1_STRING_set(ct->content, out, out_len))
+ errx(1, "ASN1_STRING_set");
+
+ vd->ccr.der = NULL;
+ if ((vd->ccr.der_len = i2d_CONTENT_TYPE(ct, &vd->ccr.der)) <= 0)
+ errx(1, "i2d_CONTENT_TYPE");
+
+ CONTENT_TYPE_free(ct);
+}
+
+
+static inline int
+ccr_mft_cmp(const struct ccr_mft *a, const struct ccr_mft *b)
+{
+ return memcmp(a->hash, b->hash, SHA256_DIGEST_LENGTH);
+}
+
+RB_GENERATE(ccr_mft_tree, ccr_mft, entry, ccr_mft_cmp);
+
+void
+ccr_insert_mft(struct ccr_mft_tree *tree, const struct mft *mft)
+{
+ struct ccr_mft *ccr_mft;
+
+ if ((ccr_mft = calloc(1, sizeof(*ccr_mft))) == NULL)
+ err(1, NULL);
+
+ if (hex_decode(mft->aki, ccr_mft->aki, sizeof(ccr_mft->aki)) != 0)
+ errx(1, "hex_decode");
+
+ if ((ccr_mft->sia = strdup(mft->sia)) == NULL)
+ err(1, NULL);
+
+ if ((ccr_mft->seqnum = strdup(mft->seqnum)) == NULL)
+ err(1, NULL);
+
+ memcpy(ccr_mft->hash, mft->mfthash, SHA256_DIGEST_LENGTH);
+
+ ccr_mft->size = mft->mftsize;
+ ccr_mft->thisupdate = mft->thisupdate;
+
+ if (RB_INSERT(ccr_mft_tree, tree, ccr_mft) != NULL)
+ errx(1, "CCR MFT tree corrupted");
+}
+
+void
+ccr_insert_roa(struct ccr_vrp_tree *tree, const struct roa *roa)
+{
+ struct vrp *vrp;
+ size_t i;
+
+ for (i = 0; i < roa->num_ips; i++) {
+ if ((vrp = calloc(1, sizeof(*vrp))) == NULL)
+ err(1, NULL);
+
+ vrp->asid = roa->asid;
+ vrp->afi = roa->ips[i].afi;
+ vrp->addr = roa->ips[i].addr;
+ vrp->maxlength = roa->ips[i].maxlength;
+
+ RB_INSERT(ccr_vrp_tree, tree, vrp);
+ }
+}
+
+/*
+ * Total ordering modeled after RFC 9582, section 4.3.3.
+ */
+static inline int
+ccr_vrp_cmp(const struct vrp *a, const struct vrp *b)
+{
+ int rv;
+
+ if (a->asid > b->asid)
+ return 1;
+ if (a->asid < b->asid)
+ return -1;
+
+ if (a->afi > b->afi)
+ return 1;
+ if (a->afi < b->afi)
+ return -1;
+
+ switch (a->afi) {
+ case AFI_IPV4:
+ rv = memcmp(&a->addr.addr, &b->addr.addr, 4);
+ if (rv)
+ return rv;
+ break;
+ case AFI_IPV6:
+ rv = memcmp(&a->addr.addr, &b->addr.addr, 16);
+ if (rv)
+ return rv;
+ break;
+ default:
+ break;
+ }
+
+ if (a->addr.prefixlen < b->addr.prefixlen)
+ return 1;
+ if (a->addr.prefixlen > b->addr.prefixlen)
+ return -1;
+
+ if (a->maxlength < b->maxlength)
+ return 1;
+ if (a->maxlength > b->maxlength)
+ return -1;
+
+ return 0;
+}
+
+RB_GENERATE(ccr_vrp_tree, vrp, entry, ccr_vrp_cmp);
+
+int
+output_ccr(FILE *out, struct validation_data *vd, struct stats *st)
+{
+ if (fwrite(vd->ccr.der, vd->ccr.der_len, 1, out) != 1)
+ err(1, "fwrite");
+
+ return 0;
+}
Index: extern.h
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/extern.h,v
diff -u -p -r1.257 extern.h
--- extern.h 14 Aug 2025 15:12:00 -0000 1.257
+++ extern.h 21 Aug 2025 21:08:51 -0000
@@ -233,6 +233,7 @@ struct mft {
char *sia; /* SIA signedObject */
char *crl; /* CRL file name */
unsigned char mfthash[SHA256_DIGEST_LENGTH];
+ size_t mftsize;
unsigned char crlhash[SHA256_DIGEST_LENGTH];
time_t signtime; /* CMS signing-time attribute */
time_t thisupdate; /* from the eContent */
@@ -459,12 +460,39 @@ struct brk {
RB_HEAD(brk_tree, brk);
RB_PROTOTYPE(brk_tree, brk, entry, brkcmp);
+struct ccr_mft {
+ RB_ENTRY(ccr_mft) entry;
+ char hash[SHA256_DIGEST_LENGTH];
+ char aki[SHA_DIGEST_LENGTH];
+ size_t size;
+ time_t thisupdate;
+ char *seqnum;
+ char *sia;
+};
+
+RB_HEAD(ccr_mft_tree, ccr_mft);
+RB_PROTOTYPE(ccr_mft_tree, ccr_mft, entry, ccr_mft_cmp);
+
+RB_HEAD(ccr_vrp_tree, vrp);
+RB_PROTOTYPE(ccr_vrp_tree, vrp, entry, ccr_vrp_cmp);
+
+struct ccr {
+ struct ccr_mft_tree mfts;
+ struct ccr_vrp_tree vrps;
+ char *mfts_hash;
+ char *vrps_hash;
+ char *vaps_hash;
+ unsigned char *der;
+ size_t der_len;
+};
+
struct validation_data {
struct vrp_tree vrps;
struct brk_tree brks;
struct vap_tree vaps;
struct vsp_tree vsps;
struct nca_tree ncas;
+ struct ccr ccr;
};
/*
@@ -973,15 +1001,24 @@ extern int outformats;
#define FORMAT_CSV 0x04
#define FORMAT_JSON 0x08
#define FORMAT_OMETRIC 0x10
+#define FORMAT_CCR 0x20
int outputfiles(struct validation_data *, struct stats *);
-int outputheader(FILE *, struct stats *);
+int outputheader(FILE *, struct validation_data *, struct stats *);
int output_bgpd(FILE *, struct validation_data *, struct stats *);
int output_bird(FILE *, struct validation_data *, struct stats *);
int output_csv(FILE *, struct validation_data *, struct stats *);
int output_json(FILE *, struct validation_data *, struct stats *);
int output_ometric(FILE *, struct validation_data *,
struct stats *);
+int output_ccr(FILE *, struct validation_data *, struct stats *);
+
+/*
+ * Canonical Cache Representation
+ */
+void ccr_insert_mft(struct ccr_mft_tree *, const struct mft *);
+void ccr_insert_roa(struct ccr_vrp_tree *, const struct roa *);
+void generate_ccr_content(struct validation_data *);
void logx(const char *fmt, ...)
__attribute__((format(printf, 1, 2)));
Index: main.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/main.c,v
diff -u -p -r1.293 main.c
--- main.c 14 Aug 2025 15:12:00 -0000 1.293
+++ main.c 21 Aug 2025 21:08:51 -0000
@@ -658,6 +658,7 @@ entity_process(struct ibuf *b, struct va
repo_stat_inc(rp, talid, type, STYPE_SEQNUM_GAP);
queue_add_from_mft(mft);
cert_remove_nca(&vd->ncas, mft->certid, rp);
+ ccr_insert_mft(&vd->ccr.mfts, mft);
mft_free(mft);
break;
case RTYPE_CRL:
@@ -671,9 +672,10 @@ entity_process(struct ibuf *b, struct va
break;
}
roa = roa_read(b);
- if (roa->valid)
+ if (roa->valid) {
roa_insert_vrps(&vd->vrps, roa, rp);
- else
+ ccr_insert_roa(&vd->ccr.vrps, roa);
+ } else
repo_stat_inc(rp, talid, type, STYPE_INVALID);
roa_free(roa);
break;
@@ -1021,6 +1023,8 @@ main(int argc, char *argv[])
RB_INIT(&vd.vaps);
RB_INIT(&vd.vsps);
RB_INIT(&vd.ncas);
+ RB_INIT(&vd.ccr.mfts);
+ RB_INIT(&vd.ccr.vrps);
/* If started as root, priv-drop to _rpki-client */
if (getuid() == 0) {
@@ -1164,6 +1168,7 @@ main(int argc, char *argv[])
err(1, "output directory %s", outputdir);
if (outformats == 0)
outformats = FORMAT_OPENBGPD;
+ outformats |= FORMAT_CCR;
}
check_fs_size(cachefd, cachedir);
@@ -1540,6 +1545,8 @@ main(int argc, char *argv[])
}
repo_stats_collect(sum_repostats, &stats.repo_stats);
+ generate_ccr_content(&vd);
+
if (outputfiles(&vd, &stats))
rc = 1;
@@ -1578,6 +1585,9 @@ main(int argc, char *argv[])
printf("Repositories: %u\n", stats.repos);
printf("New files moved into validated cache: %u\n",
stats.repo_stats.new_files);
+ printf("CCR manifest state hash: %s\n", vd.ccr.mfts_hash);
+ printf("CCR ROA payloads hash: %s\n", vd.ccr.vrps_hash);
+ printf("CCR ASPA paylaods hash: %s\n", vd.ccr.vaps_hash);
printf("Cleanup: removed %u files, %u directories\n"
"Repository cleanup: kept %u and removed %u superfluous files\n",
stats.repo_stats.del_files, stats.repo_stats.del_dirs,
Index: mft.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/mft.c,v
diff -u -p -r1.128 mft.c
--- mft.c 19 Aug 2025 11:30:20 -0000 1.128
+++ mft.c 21 Aug 2025 21:08:51 -0000
@@ -408,6 +408,7 @@ mft_parse(struct cert **out_cert, const
if ((mft = calloc(1, sizeof(*mft))) == NULL)
err(1, NULL);
mft->signtime = signtime;
+ mft->mftsize = len;
if ((mft->aki = strdup(cert->aki)) == NULL)
err(1, NULL);
@@ -498,6 +499,11 @@ mft_buffer(struct ibuf *b, const struct
io_opt_str_buffer(b, p->path);
io_str_buffer(b, p->aki);
+ io_str_buffer(b, p->seqnum);
+ io_str_buffer(b, p->sia);
+ io_simple_buffer(b, &p->thisupdate, sizeof(p->thisupdate));
+ io_simple_buffer(b, p->mfthash, sizeof(p->mfthash));
+ io_simple_buffer(b, &p->mftsize, sizeof(p->mftsize));
io_simple_buffer(b, &p->filesz, sizeof(size_t));
for (i = 0; i < p->filesz; i++) {
@@ -530,6 +536,11 @@ mft_read(struct ibuf *b)
io_read_opt_str(b, &p->path);
io_read_str(b, &p->aki);
+ io_read_str(b, &p->seqnum);
+ io_read_str(b, &p->sia);
+ io_read_buf(b, &p->thisupdate, sizeof(p->thisupdate));
+ io_read_buf(b, &p->mfthash, sizeof(p->mfthash));
+ io_read_buf(b, &p->mftsize, sizeof(p->mftsize));
io_read_buf(b, &p->filesz, sizeof(size_t));
if (p->filesz == 0)
Index: output-bgpd.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-bgpd.c,v
diff -u -p -r1.34 output-bgpd.c
--- output-bgpd.c 8 Jul 2025 14:19:21 -0000 1.34
+++ output-bgpd.c 21 Aug 2025 21:08:51 -0000
@@ -26,7 +26,7 @@ output_bgpd(FILE *out, struct validation
struct vap *vap;
size_t i;
- if (outputheader(out, st) < 0)
+ if (outputheader(out, vd, st) < 0)
return -1;
if (fprintf(out, "roa-set {\n") < 0)
Index: output-bird.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-bird.c,v
diff -u -p -r1.24 output-bird.c
--- output-bird.c 8 Jul 2025 14:19:21 -0000 1.24
+++ output-bird.c 21 Aug 2025 21:08:51 -0000
@@ -31,7 +31,7 @@ output_bird(FILE *out, struct validation
if (fprintf(out, "# For BIRD %s\n#\n", excludeaspa ? "2" : "2.16+") < 0)
return -1;
- if (outputheader(out, st) < 0)
+ if (outputheader(out, vd, st) < 0)
return -1;
if (fprintf(out, "\ndefine force_roa_table_update = %lld;\n\n"
Index: output-json.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output-json.c,v
diff -u -p -r1.54 output-json.c
--- output-json.c 8 Jul 2025 14:19:21 -0000 1.54
+++ output-json.c 21 Aug 2025 21:08:51 -0000
@@ -24,7 +24,7 @@
#include "json.h"
static void
-outputheader_json(struct stats *st)
+outputheader_json(struct validation_data *vd, struct stats *st)
{
char hn[NI_MAXHOST], tbuf[26];
struct tm *tp;
@@ -85,6 +85,9 @@ outputheader_json(struct stats *st)
json_do_int("cachedir_superfluous_files", st->repo_stats.extra_files);
json_do_int("cachedir_del_superfluous_files",
st->repo_stats.del_extra_files);
+ json_do_string("ccr_mfts_hash", vd->ccr.mfts_hash);
+ json_do_string("ccr_vrps_hash", vd->ccr.vrps_hash);
+ json_do_string("ccr_vaps_hash", vd->ccr.vaps_hash);
json_do_end();
}
@@ -153,7 +156,7 @@ output_json(FILE *out, struct validation
struct nonfunc_ca *nca;
json_do_start(out);
- outputheader_json(st);
+ outputheader_json(vd, st);
json_do_array("roas");
RB_FOREACH(v, vrp_tree, &vd->vrps) {
Index: output.c
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/output.c,v
diff -u -p -r1.41 output.c
--- output.c 8 Jul 2025 14:19:21 -0000 1.41
+++ output.c 21 Aug 2025 21:08:51 -0000
@@ -70,6 +70,7 @@ static const struct outputs {
{ FORMAT_CSV, "csv", output_csv },
{ FORMAT_JSON, "json", output_json },
{ FORMAT_OMETRIC, "metrics", output_ometric },
+ { FORMAT_CCR, "rpki.ccr", output_ccr },
{ 0, NULL, NULL }
};
@@ -238,7 +239,7 @@ set_signal_handler(void)
}
int
-outputheader(FILE *out, struct stats *st)
+outputheader(FILE *out, struct validation_data *vd, struct stats *st)
{
char hn[NI_MAXHOST], tbuf[80];
struct tm *tp;
@@ -275,11 +276,15 @@ outputheader(FILE *out, struct stats *st
if (fprintf(out,
" ]\n"
+ "# CCR manifest hash: %s\n"
+ "# CCR validated ROA payloads hash: %s\n"
+ "# CCR validated ASPA payloads hash: %s\n"
"# Manifests: %u (%u failed parse)\n"
"# Certificate revocation lists: %u\n"
"# Ghostbuster records: %u\n"
"# Repositories: %u\n"
"# VRP Entries: %u (%u unique)\n",
+ vd->ccr.mfts_hash, vd->ccr.vrps_hash, vd->ccr.vaps_hash,
st->repo_tal_stats.mfts, st->repo_tal_stats.mfts_fail,
st->repo_tal_stats.crls,
st->repo_tal_stats.gbrs,
Index: rpki-asn1.h
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/rpki-asn1.h,v
diff -u -p -r1.1 rpki-asn1.h
--- rpki-asn1.h 19 Aug 2025 11:30:20 -0000 1.1
+++ rpki-asn1.h 21 Aug 2025 21:08:51 -0000
@@ -41,6 +41,118 @@ DECLARE_ASN1_FUNCTIONS(ASProviderAttesta
/*
+ * Canonical Cache Representation (CCR)
+ * reference: TBD
+ */
+
+extern ASN1_ITEM_EXP ManifestRef_it;
+extern ASN1_ITEM_EXP ManifestRefs_it;
+extern ASN1_ITEM_EXP ROAPayloadSet_it;
+extern ASN1_ITEM_EXP ROAPayloadSets_it;
+extern ASN1_ITEM_EXP ASPAPayloadSet_it;
+extern ASN1_ITEM_EXP ASPAPayloadSets_it;
+extern ASN1_ITEM_EXP CanonicalCacheRepresentation_it;
+extern ASN1_ITEM_EXP CONTENT_TYPE_it;
+
+typedef struct {
+ ASN1_OCTET_STRING *hash;
+ ASN1_INTEGER *size;
+ ASN1_OCTET_STRING *aki;
+ ASN1_INTEGER *manifestNumber;
+ STACK_OF(ACCESS_DESCRIPTION) *location;
+} ManifestRef;
+
+DECLARE_STACK_OF(ManifestRef);
+
+#ifndef DEFINE_STACK_OF
+#define sk_ManifestRef_num(st) SKM_sk_num(ManifestRef, (st))
+#define sk_ManifestRef_push(st, i) SKM_sk_push(ManifestRef, (st), (i))
+#endif
+
+DECLARE_ASN1_FUNCTIONS(ManifestRef);
+
+typedef STACK_OF(ManifestRef) ManifestRefs;
+
+DECLARE_ASN1_FUNCTIONS(ManifestRefs);
+
+typedef struct {
+ STACK_OF(ManifestRef) *mftrefs;
+ ASN1_GENERALIZEDTIME *mostRecentUpdate;
+ ASN1_OCTET_STRING *hash;
+} ManifestState;
+
+DECLARE_ASN1_FUNCTIONS(ManifestState);
+
+typedef struct {
+ ASN1_INTEGER *asID;
+ STACK_OF(ROAIPAddressFamily) *ipAddrBlocks;
+} ROAPayloadSet;
+
+DECLARE_STACK_OF(ROAPayloadSet);
+
+#ifndef DEFINE_STACK_OF
+#define sk_ROAPayloadSet_num(st) SKM_sk_num(ROAPayloadSet, (st))
+#define sk_ROAPayloadSet_push(st, i) SKM_sk_push(ROAPayloadSet, (st), (i))
+#endif
+
+DECLARE_ASN1_FUNCTIONS(ROAPayloadSet);
+
+typedef STACK_OF(ROAPayloadSet) ROAPayloadSets;
+
+DECLARE_ASN1_FUNCTIONS(ROAPayloadSets);
+
+typedef struct {
+ STACK_OF(ROAPayloadSet) *rps;
+ ASN1_OCTET_STRING *hash;
+} ROAPayloadState;
+
+DECLARE_ASN1_FUNCTIONS(ROAPayloadState);
+
+typedef struct {
+ ASN1_INTEGER *asID;
+ STACK_OF(ASN1_INTEGER) *providers;
+} ASPAPayloadSet;
+
+DECLARE_STACK_OF(ASPAPayloadSet);
+
+#ifndef DEFINE_STACK_OF
+#define sk_ASPAPayloadSet_num(st) SKM_sk_num(ASPAPayloadSet, (st))
+#define sk_ASPAPayloadSet_push(st, i) SKM_sk_push(ASPAPayloadSet, (st), (i))
+#endif
+
+DECLARE_ASN1_FUNCTIONS(ASPAPayloadSet);
+
+typedef STACK_OF(ASPAPayloadSet) ASPAPayloadSets;
+
+DECLARE_ASN1_FUNCTIONS(ASPAPayloadSets);
+
+typedef struct {
+ STACK_OF(ASPAPayloadSet) *aps;
+ ASN1_OCTET_STRING *hash;
+} ASPAPayloadState;
+
+DECLARE_ASN1_FUNCTIONS(ASPAPayloadState);
+
+typedef struct {
+ ASN1_INTEGER *version;
+ ASN1_OBJECT *hashAlg;
+ ASN1_GENERALIZEDTIME *producedAt;
+ ManifestState *mfts;
+ ROAPayloadState *vrps;
+ ASPAPayloadState *vaps;
+} CanonicalCacheRepresentation;
+
+DECLARE_ASN1_FUNCTIONS(CanonicalCacheRepresentation);
+
+typedef struct {
+ ASN1_OBJECT *oid;
+ ASN1_OCTET_STRING *content;
+} CONTENT_TYPE;
+
+DECLARE_ASN1_FUNCTIONS(CONTENT_TYPE);
+
+
+/*
* RPKI Manifest
* reference: RFC 9286.
*/
@@ -91,20 +203,27 @@ typedef struct {
ASN1_INTEGER *maxLength;
} ROAIPAddress;
+DECLARE_ASN1_FUNCTIONS(ROAIPAddress);
DECLARE_STACK_OF(ROAIPAddress);
+#ifndef DEFINE_STACK_OF
+#define sk_ROAIPAddress_num(st) SKM_sk_num(ROAIPAddress, (st))
+#define sk_ROAIPAddress_push(st, i) SKM_sk_push(ROAIPAddress, (st), (i))
+#define sk_ROAIPAddress_value(st, i) SKM_sk_value(ROAIPAddress, (st), (i))
+#endif
+
typedef struct {
ASN1_OCTET_STRING *addressFamily;
STACK_OF(ROAIPAddress) *addresses;
} ROAIPAddressFamily;
+DECLARE_ASN1_FUNCTIONS(ROAIPAddressFamily);
DECLARE_STACK_OF(ROAIPAddressFamily);
#ifndef DEFINE_STACK_OF
-#define sk_ROAIPAddress_num(st) SKM_sk_num(ROAIPAddress, (st))
-#define sk_ROAIPAddress_value(st, i) SKM_sk_value(ROAIPAddress, (st), (i))
-
-#define sk_ROAIPAddressFamily_num(st) SKM_sk_num(ROAIPAddressFamily, (st))
+#define sk_ROAIPAddressFamily_num(st) SKM_sk_num(ROAIPAddressFamily, (st))
+#define sk_ROAIPAddressFamily_push(st, i) \
+ SKM_sk_push(ROAIPAddressFamily, (st), (i))
#define sk_ROAIPAddressFamily_value(st, i) \
SKM_sk_value(ROAIPAddressFamily, (st), (i))
#endif
Index: rpki-client.8
===================================================================
RCS file: /cvs/src/usr.sbin/rpki-client/rpki-client.8,v
diff -u -p -r1.127 rpki-client.8
--- rpki-client.8 2 Aug 2025 13:24:16 -0000 1.127
+++ rpki-client.8 21 Aug 2025 21:08:51 -0000
@@ -318,6 +318,8 @@ default skiplist file, unless
is specified.
.It Pa /var/cache/rpki-client
cached repository data.
+.It Pa /var/db/rpki-client/rpki.ccr
+DER-encoded canonical cache representation file.
.It Pa /var/db/rpki-client/openbgpd
default roa-set output file.
.El
rpki-client: Canonical Cache Representation