Index | Thread | Search

From:
Theo Buehler <tb@theobuehler.org>
Subject:
Re: rpki-client: Canonical Cache Representation
To:
Job Snijders <job@openbsd.org>
Cc:
tech@openbsd.org
Date:
Fri, 22 Aug 2025 07:14:10 +0200

Download raw body.

Thread
On Thu, Aug 21, 2025 at 09:26:19PM +0000, Job Snijders wrote:
> 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.

It's not that bad anymore :)

> 2/ ASN1_ITEM_ref() is an undocumented libcrypto asn1.h macro.

Unlike all the other undocumented libcrypto asn1.h macros that we use? :)

> OK?

There's still a bunch of things that need fixing. Mostly cosmetics,
but there are a couple of bugs.

> 
> 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");

		errx(1, "ASN1_OCTET_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)

I think the wrapping you chose for roas and aspas is nicer:

	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");

ASN1_SEQUENCE(ROAIPAddress) = {
        ASN1_SIMPLE(ROAIPAddress, address, ASN1_BIT_STRING),
        ASN1_OPT(ROAIPAddress, maxLength, ASN1_INTEGER),
} ASN1_SEQUENCE_END(ROAIPAddress);

implies that ripa->address != NULL since it's a non-optional bit
string and thus is leaked below in ripa->address = addr. 

So drop the addr variable

> +
> +	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))

	if (!ASN1_BIT_STRING_set(ripa->address, 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;

and do this (leave the XXX as a comment)

	ripa->address->flags |= ASN1_STRING_FLAG_BITS_LEFT | unused_bits;

	/* XXX - assert that unused bits are zero */

> +
> +	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. */
			assert(vrp->afi == AFI_IPV4 || vrp->afi = AFI_IPV6);

> +			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");

			errx(1, "ASN1_INTEGER_new");

> +
> +		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)

why build_ and not generate_?

> +{
> +	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);
> +

I would start the function with this:

> +	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");

... and build (or generate) and serialize the ccr here:

	ccr = build_ccr(vd);

	if ((out_len = i2d_CanonicalCacheRepresentation(ccr, &out)) <= 0)
		err(1, "i2d_CanonicalCacheRepresentation");

	CanonicalCacheRepresentation_free(ccr);

> +
> +	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");

There's a sign bug: vd->ccr.der_len is a size_t (unsigned).
i2d_CONTENT_TYPE can return -1 which will become large after
integer promotion and the error isn't hit.

Two possible fixes: use an auxiliary int len (don't reuse out_len):

	if ((len = i2d_CONTENT_TYPE(ct, &vd->ccr.der)) <= 0)
		errx(1, "i2d_CONTENT_TYPE");
	vd->ccr.der_len = len;

Or switch the type of der_len in extern.h to int. I would probably use
len.

> +		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);

	memcpy(ccr_mft->hash, mft->mfthash, sizeof(ccr_mft->hash));

> +
> +	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;

I'd abort() - this is kind of redundnat with the assert above,
but I would keep that.

> +	}
> +
> +	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)

output_ccr or output_ccr_content?

> +{
> +	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;

If you didn't use the auxiliary int len in generate_ccr_content(),
change this

> +};
> +
>  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"

Not sure I like this placement between tals and manifests

Also, why do we display the number of GBR but not the one of ASPA?  :)

>  	    "# 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
>