From: Theo Buehler Subject: Re: rpki-client: Canonical Cache Representation To: Job Snijders Cc: tech@openbsd.org Date: Fri, 22 Aug 2025 07:14:10 +0200 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 > + * > + * 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 > +#include > + > +#include > +#include > +#include > +#include > +#include > + > +#include > +#include > +#include > +#include > + > +#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 >