From: Job Snijders Subject: rpki-client: implement CCR decoder for filemode To: tech@openbsd.org Date: Wed, 27 Aug 2025 09:59:42 +0000 This diff adds filemode support for decoding .ccr files and printing out the contents in JSON or 'plain' format. It also checks whether the CCR is in canonical form. Index: ccr.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/ccr.c,v diff -u -p -r1.2 ccr.c --- ccr.c 23 Aug 2025 11:16:50 -0000 1.2 +++ ccr.c 27 Aug 2025 09:55:49 -0000 @@ -351,6 +351,7 @@ append_cached_vrp(STACK_OF(ROAIPAddress) static ROAPayloadState * generate_roapayloadstate(struct validation_data *vd) { + struct ccr *ccr = &vd->ccr; ROAPayloadState *vrps; struct vrp *prev, *vrp; ROAPayloadSet *rp; @@ -361,7 +362,7 @@ generate_roapayloadstate(struct validati errx(1, "ROAPayloadState_new"); prev = NULL; - RB_FOREACH(vrp, ccr_vrp_tree, &vd->ccr.vrps) { + RB_FOREACH(vrp, ccr_vrp_tree, &ccr->vrps) { if (prev == NULL || prev->asid != vrp->asid) { if ((rp = ROAPayloadSet_new()) == NULL) errx(1, "ROAPayloadSet_new"); @@ -631,4 +632,546 @@ output_ccr_der(FILE *out, struct validat err(1, "fwrite"); return 0; +} + +void +ccr_free(struct ccr *ccr) +{ + struct ccr_mft *ccr_mft, *tmp_ccr_mft; + struct vrp *vrp, *tmp_vrp; + struct vap *vap, *tmp_vap; + + if (ccr == NULL) + return; + + RB_FOREACH_SAFE(ccr_mft, ccr_mft_tree, &ccr->mfts, tmp_ccr_mft) { + RB_REMOVE(ccr_mft_tree, &ccr->mfts, ccr_mft); + free(ccr_mft->seqnum); + free(ccr_mft->sia); + free(ccr_mft); + } + + RB_FOREACH_SAFE(vrp, ccr_vrp_tree, &ccr->vrps, tmp_vrp) { + RB_REMOVE(ccr_vrp_tree, &ccr->vrps, vrp); + free(vrp); + } + + RB_FOREACH_SAFE(vap, vap_tree, &ccr->vaps, tmp_vap) { + RB_REMOVE(vap_tree, &ccr->vaps, vap); + free(vap->providers); + free(vap); + } + + free(ccr->mfts_hash); + free(ccr->vrps_hash); + free(ccr->vaps_hash); + free(ccr->der); + + free(ccr); +} + +static int +parse_mft_refs(const char *fn, struct ccr *ccr, + const STACK_OF(ManifestRef) *refs) +{ + ManifestRef *ref; + struct ccr_mft *ccr_mft; + int i, refs_cnt; + const ACCESS_DESCRIPTION *ad; + int rc = 0; + + if ((refs_cnt = sk_ManifestRef_num(refs)) <= 0) { + warnx("%s: missing ManifestRefs", fn); + goto out; + } + + RB_INIT(&ccr->mfts); + + for (i = 0; i < refs_cnt; i++) { + if ((ccr_mft = calloc(1, sizeof(struct ccr_mft))) == NULL) + err(1, NULL); + + ref = sk_ManifestRef_value(refs, i); + + if (ASN1_STRING_length(ref->hash) != SHA256_DIGEST_LENGTH) { + warnx("%s: manifest ref #%d corrupted", fn, i); + goto out; + } + memcpy(ccr_mft->hash, ref->hash->data, SHA256_DIGEST_LENGTH); + + if (ASN1_STRING_length(ref->aki) == SHA_DIGEST_LENGTH) { + memcpy(ccr_mft->aki, ref->aki->data, + SHA_DIGEST_LENGTH); + } else { + warnx("%s: manifest ref #%d corrupted", fn, i); + goto out; + } + + long long size = 0; + if (!ASN1_INTEGER_get_int64(&size, ref->size)) { + warnx("%s: manifest ref #%d corrupted", fn, i); + goto out; + } + if (size <= 0 || size > MAX_FILE_SIZE) { + warnx("%s: manifest ref #%d corrupted", fn, i); + goto out; + } + ccr_mft->size = size; + + ccr_mft->seqnum = x509_convert_seqnum(fn, "manifest number", + ref->manifestNumber); + if (ccr_mft->seqnum == NULL) + goto out; + + if (sk_ACCESS_DESCRIPTION_num(ref->location) != 1) { + warnx("%s: unexpected number of locations", fn); + goto out; + } + + ad = sk_ACCESS_DESCRIPTION_value(ref->location, 0); + if (ad == NULL) + goto out; + if (!x509_location(fn, "SIA: signedObject", ad->location, + &ccr_mft->sia)) + goto out; + + if (RB_INSERT(ccr_mft_tree, &ccr->mfts, ccr_mft) != NULL) { + warnx("%s: manifest state corrupted", fn); + free(ccr_mft); + goto out; + } + } + + rc = 1; + out: + return rc; +} + +static char * +parse_asn1_hash(const char *fn, const char *descr, + const ASN1_OCTET_STRING *astr, const ASN1_ITEM *it, void *val) +{ + const unsigned char *hash; + unsigned char real_hash[SHA256_DIGEST_LENGTH]; + char *hex = NULL; + + if (ASN1_STRING_length(astr) != SHA256_DIGEST_LENGTH) { + warnx("%s: %s state hash format invalid", fn, descr); + goto out; + } + if ((hash = ASN1_STRING_get0_data(astr)) == NULL) { + warnx("%s: %s hash is null", fn, descr); + goto out; + } + + if (!ASN1_item_digest(it, EVP_sha256(), val, real_hash, NULL)) + errx(1, "ASN1_item_digest"); + if (memcmp(hash, real_hash, SHA256_DIGEST_LENGTH) != 0) { + warnx("%s: corrupted %s state", fn, descr); + goto out; + } + + hex = hex_encode(hash, SHA256_DIGEST_LENGTH); + out: + return hex; +} + +static int +parse_manifeststate(const char *fn, struct ccr *ccr, const ManifestState *state) +{ + int rc = 0; + + ccr->mfts_hash = parse_asn1_hash(fn, "ManifestState", state->hash, + ASN1_ITEM_rptr(ManifestRefs), state->mftrefs); + if (ccr->mfts_hash == NULL) + goto out; + + if (ASN1_STRING_length(state->mostRecentUpdate) != GENTIME_LENGTH) { + warnx("%s: mostRecentUpdate time format invalid", fn); + goto out; + } + if (!x509_get_time(state->mostRecentUpdate, &ccr->mft_lastupdate)) { + warnx("%s: parsing CCR mostRecentUpdate failed", fn); + goto out; + } + + if (!parse_mft_refs(fn, ccr, state->mftrefs)) + goto out; + + rc = 1; + out: + return rc; +} + +static int +parse_roa_addresses(const char *fn, struct ccr *ccr, int asid, enum afi afi, + const STACK_OF(ROAIPAddress) *addrs) +{ + const ROAIPAddress *r; + struct vrp *fnd, *prev, *vrp; + uint64_t maxlen; + int addrs_cnt, i, rc = 0; + + if ((addrs_cnt = sk_ROAIPAddress_num(addrs)) <= 0) { + warnx("%s: missing ROAIPAddress", fn); + goto out; + } + + prev = NULL; + for (i = 0; i < addrs_cnt; i++) { + r = sk_ROAIPAddress_value(addrs, i); + + if ((vrp = calloc(1, sizeof(struct vrp))) == NULL) + err(1, NULL); + + vrp->asid = asid; + vrp->afi = afi; + + if (!ip_addr_parse(r->address, afi, fn, &vrp->addr)) { + warnx("%s: invalid address in ROAPayload", fn); + goto out; + } + + maxlen = vrp->addr.prefixlen; + if (r->maxLength != NULL) { + if (!ASN1_INTEGER_get_uint64(&maxlen, r->maxLength)) { + warnx("%s: ASN1_INTEGER_get_uint64 failed", fn); + goto out; + } + if (vrp->addr.prefixlen > maxlen) { + warnx("%s: invalid maxLength", fn); + goto out; + } + if (maxlen > ((afi == AFI_IPV4) ? 32 : 128)) { + warnx("%s: maxLength too large", fn); + goto out; + } + vrp->maxlength = maxlen; + } + + if (prev != NULL) { + if (ccr_vrp_cmp(vrp, prev) != 1) { + warnx("%s: misordered ROAIPAddressFamily", fn); + goto out; + } + } + + if ((fnd = RB_INSERT(ccr_vrp_tree, &ccr->vrps, vrp)) != NULL) { + warnx("%s: duplicate ROAIPAddress", fn); + free(vrp); + goto out; + } + + prev = vrp; + } + + rc = 1; + out: + return rc; +} + +static int +parse_roa_ipaddrb(const char *fn, struct ccr *ccr, int asid, + const STACK_OF(ROAIPAddressFamily) *ipaddrblocks) +{ + const ROAIPAddressFamily *ripaf; + enum afi afi; + int ipv4_seen = 0, ipv6_seen = 0; + int i, rc = 0, ipb_cnt; + + ipb_cnt = sk_ROAIPAddressFamily_num(ipaddrblocks); + if (ipb_cnt != 1 && ipb_cnt != 2) { + warnx("%s: unexpected ipAddrBlocks count for AS %d", fn, asid); + goto out; + } + + for (i = 0; i < ipb_cnt; i++) { + ripaf = sk_ROAIPAddressFamily_value(ipaddrblocks, i); + + if (!ip_addr_afi_parse(fn, ripaf->addressFamily, &afi)) { + warnx("%s: invalid afi for AS %d", fn, asid); + goto out; + } + + switch (afi) { + case AFI_IPV4: + if (ipv6_seen > 0) { + warnx("%s: misordered IPv4 addressFamily for AS" + " %d", fn, asid); + goto out; + } + if (ipv4_seen++ > 0) { + warnx("%s: IPv4 addressFamily duplicate for AS" + " %d", fn, asid); + goto out; + } + break; + case AFI_IPV6: + if (ipv6_seen++ > 0) { + warnx("%s: IPv6 addressFamily duplicate for AS" + " %d", fn, asid); + goto out; + } + break; + } + + if (!parse_roa_addresses(fn, ccr, asid, afi, ripaf->addresses)) + goto out; + } + + rc = 1; + out: + return rc; +} + +static int +parse_roa_payloads(const char *fn, struct ccr *ccr, + const STACK_OF(ROAPayloadSet) *rps) +{ + ROAPayloadSet *rp; + int i, rc = 0, rps_cnt; + + if ((rps_cnt = sk_ROAPayloadSet_num(rps)) <= 0) { + warnx("%s: missing ROAPayloadSets", fn); + goto out; + } + + RB_INIT(&ccr->vrps); + + for (i = 0; i < rps_cnt; i++) { + int asid; + rp = sk_ROAPayloadSet_value(rps, i); + + if (!as_id_parse(rp->asID, &asid)) { + warnx("%s: malformed asID in ROAPayloadSet", fn); + goto out; + } + + if (!parse_roa_ipaddrb(fn, ccr, asid, rp->ipAddrBlocks)) + goto out; + } + + rc = 1; + out: + return rc; +} + +static int +parse_roastate(const char *fn, struct ccr *ccr, const ROAPayloadState *state) +{ + int rc = 0; + + ccr->vrps_hash = parse_asn1_hash(fn, "ROAPayloadState", state->hash, + ASN1_ITEM_rptr(ROAPayloadSets), state->rps); + if (ccr->vrps_hash == NULL) + goto out; + + if (!parse_roa_payloads(fn, ccr, state->rps)) + goto out; + + rc = 1; + out: + return rc; +} + +static int +parse_aspa_providers(const char *fn, struct ccr *ccr, int asid, + STACK_OF(ASN1_INTEGER) *providers) +{ + struct vap *fnd, *vap; + ASN1_INTEGER *aint; + uint32_t prev, provider; + int i, p_cnt, rc = 0; + + if ((p_cnt = sk_ASN1_INTEGER_num(providers)) <= 0) { + warnx("%s: AS %d ASPAPayloadSet providers missing", fn, asid); + goto out; + } + + if ((vap = calloc(1, sizeof(struct vap))) == NULL) + err(1, NULL); + + vap->custasid = asid; + vap->num_providers = p_cnt; + + if ((vap->providers = calloc(p_cnt, sizeof(vap->providers[0]))) == NULL) + err(1, NULL); + + for (i = 0; i < p_cnt; i++) { + aint = sk_ASN1_INTEGER_value(providers, i); + + if (!as_id_parse(aint, &provider)) { + warnx("%s: AS %d malformed ASPA provider", fn, asid); + goto out; + } + + if (i > 0) { + if (provider <= prev) { + warnx("%s: AS %d providers invalid", fn, asid); + goto out; + } + } + + prev = provider; + vap->providers[i] = provider; + } + + if ((fnd = RB_INSERT(vap_tree, &ccr->vaps, vap)) != NULL) { + warnx("%s: duplicate ASPAPayloadSet", fn); + free(vap); + goto out; + } + + rc = 1; + out: + return rc; +} + +static int +parse_aspa_payloads(const char *fn, struct ccr *ccr, + const STACK_OF(ASPAPayloadSet) *aps) +{ + ASPAPayloadSet *a; + int asid, prev_asid; + int i, rc = 0, aps_cnt; + + if ((aps_cnt = sk_ASPAPayloadSet_num(aps)) <= 0) { + warnx("%s: missing ASPAPayloadSets", fn); + goto out; + } + + RB_INIT(&ccr->vaps); + + prev_asid = -1; + for (i = 0; i < aps_cnt; i++) { + a = sk_ASPAPayloadSet_value(aps, i); + + if (!as_id_parse(a->asID, &asid)) { + warnx("%s: malformed asID in ASPAPayloadSet", fn); + goto out; + } + + if (prev_asid != -1) { + if (asid <= prev_asid) { + warnx("%s: ASPAPayloadState misordered", fn); + goto out; + } + prev_asid = asid; + } + + if (!parse_aspa_providers(fn, ccr, asid, a->providers)) + goto out; + } + + rc = 1; + out: + return rc; +} + +static int +parse_aspastate(const char *fn, struct ccr *ccr, const ASPAPayloadState *state) +{ + int rc = 0; + + ccr->vaps_hash = parse_asn1_hash(fn, "ASPAPayloadState", state->hash, + ASN1_ITEM_rptr(ASPAPayloadSets), state->aps); + if (ccr->vaps_hash == NULL) + goto out; + + if (!parse_aspa_payloads(fn, ccr, state->aps)) + goto out; + + rc = 1; + out: + return rc; +} + +struct ccr * +ccr_parse(const char *fn, const unsigned char *der, size_t len) +{ + const unsigned char *oder; + ContentInfo *ci = NULL; + const unsigned char *con_data; + size_t con_len; + CanonicalCacheRepresentation *ccr_asn1 = NULL; + struct ccr *ccr = NULL; + int rc = 1; + + /* just fail for empty buffers, the warning was printed elsewhere */ + if (der == NULL) + return NULL; + + oder = der; + if ((ci = d2i_ContentInfo(NULL, &der, len)) == NULL) { + warnx("%s: d2i_ContentInfo", fn); + goto out; + } + if (der != oder + len) { + warnx("%s: %td bytes trailing garbage", fn, oder + len - der); + goto out; + } + + if (OBJ_cmp(ci->contentType, ccr_oid) != 0) { + char buf[128]; + + OBJ_obj2txt(buf, sizeof(buf), ci->contentType, 1); + warnx("%s: unexpected OID: got %s, want 1.3.6.1.4.1.41948.825", + fn, buf); + goto out; + } + + con_data = ci->content->data; + con_len = ci->content->length; + ccr_asn1 = d2i_CanonicalCacheRepresentation(NULL, &con_data, con_len); + if (ccr_asn1 == NULL) { + warnx("%s: d2i_CanonicalCacheRepresentation failed", fn); + goto out; + } + + if (!valid_econtent_version(fn, ccr_asn1->version, 0)) + goto out; + + if (OBJ_obj2nid(ccr_asn1->hashAlg) != NID_sha256) { + warnx("%s: hashAlg: want SHA256 object, have %s", fn, + nid2str(OBJ_obj2nid(ccr_asn1->hashAlg))); + goto out; + } + + if ((ccr = calloc(1, sizeof(*ccr))) == NULL) + err(1, NULL); + + if (ASN1_STRING_length(ccr_asn1->producedAt) != GENTIME_LENGTH) { + warnx("%s: embedded from time format invalid", fn); + goto out; + } + if (!x509_get_time(ccr_asn1->producedAt, &ccr->producedat)) { + warnx("%s: parsing CCR producedAt failed", fn); + goto out; + } + + if (ccr_asn1->mfts != NULL) { + if (!parse_manifeststate(fn, ccr, ccr_asn1->mfts)) + goto out; + } + + if (ccr_asn1->vrps != NULL) { + if (!parse_roastate(fn, ccr, ccr_asn1->vrps)) + goto out; + } + + if (ccr_asn1->vaps != NULL) { + if (!parse_aspastate(fn, ccr, ccr_asn1->vaps)) + goto out; + } + + rc = 0; + out: + if (rc) { + CanonicalCacheRepresentation_free(ccr_asn1); + ContentInfo_free(ci); + ccr_free(ccr); + ccr = NULL; + } + + return ccr; } Index: encoding.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/encoding.c,v diff -u -p -r1.13 encoding.c --- encoding.c 15 May 2022 15:00:53 -0000 1.13 +++ encoding.c 27 Aug 2025 09:55:49 -0000 @@ -47,7 +47,7 @@ load_file(const char *name, size_t *len) return NULL; if (fstat(fd, &st) != 0) goto err; - if (st.st_size <= 0 || st.st_size > MAX_FILE_SIZE) { + if (st.st_size <= 0 || st.st_size > MAX_DISK_FILE_SIZE) { errno = EFBIG; goto err; } Index: extern.h =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/extern.h,v diff -u -p -r1.260 extern.h --- extern.h 24 Aug 2025 12:17:12 -0000 1.260 +++ extern.h 27 Aug 2025 09:55:49 -0000 @@ -202,6 +202,7 @@ enum rtype { RTYPE_TAK, RTYPE_GEOFEED, RTYPE_SPL, + RTYPE_CCR, }; enum location { @@ -477,10 +478,13 @@ RB_HEAD(ccr_vrp_tree, vrp); RB_PROTOTYPE(ccr_vrp_tree, vrp, entry, ccr_vrp_cmp); struct ccr { + time_t producedat; + time_t mft_lastupdate; /* mostRecentUpdate */ struct ccr_mft_tree mfts; - struct ccr_vrp_tree vrps; char *mfts_hash; + struct ccr_vrp_tree vrps; char *vrps_hash; + struct vap_tree vaps; char *vaps_hash; unsigned char *der; size_t der_len; @@ -697,6 +701,7 @@ extern ASN1_OBJECT *aspa_oid; extern ASN1_OBJECT *tak_oid; extern ASN1_OBJECT *geofeed_oid; extern ASN1_OBJECT *spl_oid; +extern ASN1_OBJECT *ccr_oid; extern int verbose; extern int noop; @@ -1016,6 +1021,9 @@ int output_ccr_der(FILE *, struct vali /* * Canonical Cache Representation */ +void ccr_free(struct ccr *); +void ccr_print(struct ccr *); +struct ccr * ccr_parse(const char *, const unsigned char *, size_t); void ccr_insert_mft(struct ccr_mft_tree *, const struct mft *); void ccr_insert_roa(struct ccr_vrp_tree *, const struct roa *); void serialize_ccr_content(struct validation_data *); @@ -1053,6 +1061,7 @@ int mkpathat(int, const char *); /* Min/Max acceptable file size */ #define MIN_FILE_SIZE 100 #define MAX_FILE_SIZE 8000000 +#define MAX_DISK_FILE_SIZE 10 * MAX_FILE_SIZE /* Maximum number of FileNameAndHash entries per RSC checklist. */ #define MAX_CHECKLIST_ENTRIES 100000 Index: filemode.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/filemode.c,v diff -u -p -r1.67 filemode.c --- filemode.c 1 Aug 2025 16:33:58 -0000 1.67 +++ filemode.c 27 Aug 2025 09:55:49 -0000 @@ -413,6 +413,7 @@ proc_parser_file(char *file, unsigned ch static int num; struct aspa *aspa = NULL; struct cert *cert = NULL; + struct ccr *ccr = NULL; struct crl *crl = NULL; struct gbr *gbr = NULL; struct geofeed *geofeed = NULL; @@ -482,6 +483,12 @@ proc_parser_file(char *file, unsigned ch notbefore = &cert->notbefore; notafter = &cert->notafter; break; + case RTYPE_CCR: + ccr = ccr_parse(file, buf, len); + if (ccr == NULL) + break; + ccr_print(ccr); + break; case RTYPE_CER: cert = cert_parse(file, buf, len); if (cert == NULL) @@ -717,6 +724,7 @@ proc_parser_file(char *file, unsigned ch out: aspa_free(aspa); cert_free(cert); + ccr_free(ccr); crl_free(crl); gbr_free(gbr); geofeed_free(geofeed); Index: main.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/main.c,v diff -u -p -r1.295 main.c --- main.c 24 Aug 2025 12:17:12 -0000 1.295 +++ main.c 27 Aug 2025 09:55:49 -0000 @@ -1023,6 +1023,7 @@ 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); Index: mft.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/mft.c,v diff -u -p -r1.130 mft.c --- mft.c 24 Aug 2025 12:17:12 -0000 1.130 +++ mft.c 27 Aug 2025 09:55:50 -0000 @@ -58,8 +58,6 @@ ASN1_SEQUENCE(FileAndHash) = { ASN1_SIMPLE(FileAndHash, hash, ASN1_BIT_STRING), } ASN1_SEQUENCE_END(FileAndHash); -#define GENTIME_LENGTH 15 - /* * Determine rtype corresponding to file extension. Returns RTYPE_INVALID * on error or unknown extension. @@ -95,6 +93,8 @@ rtype_from_file_extension(const char *fn return RTYPE_GEOFEED; if (strcasecmp(fn + sz - 4, ".spl") == 0) return RTYPE_SPL; + if (strcasecmp(fn + sz - 4, ".ccr") == 0) + return RTYPE_CCR; return RTYPE_INVALID; } Index: print.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/print.c,v diff -u -p -r1.65 print.c --- print.c 20 Jul 2025 14:23:44 -0000 1.65 +++ print.c 27 Aug 2025 09:55:50 -0000 @@ -883,3 +883,171 @@ geofeed_print(const struct cert *c, cons if (outformats & FORMAT_JSON) json_do_end(); } + +static void +print_ccr_mftstate(struct ccr *ccr) +{ + char *aki, *hash; + struct ccr_mft *ccr_mft; + int i = 0; + + if (base64_encode(ccr->mfts_hash, SHA256_DIGEST_LENGTH, &hash) == -1) + errx(1, "base64_encode"); + + if (outformats & FORMAT_JSON) { + json_do_object("manifest_state", 0); + json_do_int("most_recent_update", ccr->mft_lastupdate); + json_do_string("hash", hash); + json_do_array("refs"); + } else { + printf("Manifest state hash: %s\n", hash); + printf("Manifest last update: %s\n", + time2str(ccr->mft_lastupdate)); + printf("Manifest references: \n"); + } + free(hash); + + RB_FOREACH(ccr_mft, ccr_mft_tree, &ccr->mfts) { + if (base64_encode(ccr_mft->hash, SHA256_DIGEST_LENGTH, &hash) + == -1) + errx(1, "base64_encode"); + aki = hex_encode(ccr_mft->aki, SHA_DIGEST_LENGTH); + + if (outformats & FORMAT_JSON) { + json_do_object("ref", 1); + json_do_string("hash", hash); + json_do_uint("size", ccr_mft->size); + json_do_string("aki", aki); + json_do_string("seqnum", ccr_mft->seqnum); + json_do_string("sia", ccr_mft->sia); + json_do_end(); + } else { + if (++i > 1) + printf("%26s-\n", ""); + printf("%26shash: %s\n", "", hash); + printf("%26ssize: %zu\n", "", ccr_mft->size); + printf("%26saki: %s\n", "", aki); + printf("%26sseqnum: %s\n", "", ccr_mft->seqnum); + printf("%26ssia: %s\n", "", ccr_mft->sia); + } + + free(aki); + free(hash); + } + if (outformats & FORMAT_JSON) + json_do_end(); +} + +static void +print_ccr_roastate(struct ccr *ccr) +{ + char buf[64], *hash; + struct vrp *vrp; + + if (base64_encode(ccr->vrps_hash, SHA256_DIGEST_LENGTH, &hash) == -1) + errx(1, "base64_encode"); + + if (outformats & FORMAT_JSON) { + json_do_object("roapayload_state", 0); + json_do_string("hash", hash); + json_do_array("vrps"); + } else { + printf("ROA payload state hash: %s\n", hash); + printf("ROA payload entries:\n"); + } + free(hash); + + RB_FOREACH(vrp, ccr_vrp_tree, &ccr->vrps) { + ip_addr_print(&vrp->addr, vrp->afi, buf, sizeof(buf)); + + if (outformats & FORMAT_JSON) { + json_do_object("vrp", 1); + json_do_string("prefix", buf); + json_do_int("asn", vrp->asid); + if (vrp->maxlength) + json_do_int("maxlen", vrp->maxlength); + json_do_end(); + } else { + printf("%26s%s", "", buf); + if (vrp->maxlength) + printf("-%hhu", vrp->maxlength); + printf(" AS %u\n", vrp->asid); + } + + } + if (outformats & FORMAT_JSON) + json_do_end(); +} + +static void +print_ccr_aspastate(struct ccr *ccr) +{ + char *hash; + struct vap *vap; + size_t i; + + if (base64_encode(ccr->vaps_hash, SHA256_DIGEST_LENGTH, &hash) == -1) + errx(1, "base64_encode"); + + if (outformats & FORMAT_JSON) { + json_do_object("aspapayload_state", 0); + json_do_string("hash", hash); + json_do_array("vaps"); + } else { + printf("ASPA payload state hash: %s\n", hash); + printf("ASPA payload entries:\n"); + } + free(hash); + + RB_FOREACH(vap, vap_tree, &ccr->vaps) { + if (outformats & FORMAT_JSON) { + json_do_object("vap", 1); + json_do_uint("customer_asid", vap->custasid); + json_do_array("providers"); + } else { + printf("%26s", ""); + printf("customer: %d providers: ", vap->custasid); + } + + for (i = 0; i < vap->num_providers; i++) { + if (outformats & FORMAT_JSON) + json_do_uint("provider", vap->providers[i]); + else { + if (i > 0) + printf(", "); + printf("%u", vap->providers[i]); + } + } + if (outformats & FORMAT_JSON) { + json_do_end(); + json_do_end(); + } else + printf("\n"); + } + + if (outformats & FORMAT_JSON) { + json_do_end(); + json_do_end(); + } +} + +void +ccr_print(struct ccr *ccr) +{ + if (outformats & FORMAT_JSON) { + json_do_string("type", "ccr"); + json_do_int("produced_at", ccr->producedat); + } else { + printf("CCR produced at: %s\n", + time2str(ccr->producedat)); + } + + if (ccr->mfts_hash != NULL) + print_ccr_mftstate(ccr); + + if (ccr->vrps_hash != NULL) + print_ccr_roastate(ccr); + + if (ccr->vaps_hash != NULL) + print_ccr_aspastate(ccr); +} Index: rpki-asn1.h =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/rpki-asn1.h,v diff -u -p -r1.2 rpki-asn1.h --- rpki-asn1.h 23 Aug 2025 09:13:14 -0000 1.2 +++ rpki-asn1.h 27 Aug 2025 09:55:50 -0000 @@ -24,6 +24,8 @@ #include #include +#define GENTIME_LENGTH 15 + /* * Autonomous System Provider Authorization (ASPA) * reference: draft-ietf-sidrops-aspa-profile @@ -67,6 +69,7 @@ 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)) +#define sk_ManifestRef_value(st, i) SKM_sk_value(ManifestRef, (st), (i)) #endif DECLARE_ASN1_FUNCTIONS(ManifestRef); @@ -93,6 +96,7 @@ 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)) +#define sk_ROAPayloadSet_value(st, i) SKM_sk_value(ROAPayloadSet, (st), (i)) #endif DECLARE_ASN1_FUNCTIONS(ROAPayloadSet); @@ -118,6 +122,7 @@ 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)) +#define sk_ASPAPayloadSet_value(st, i) SKM_sk_value(ASPAPayloadSet, (st), (i)) #endif DECLARE_ASN1_FUNCTIONS(ASPAPayloadSet); Index: x509.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/x509.c,v diff -u -p -r1.117 x509.c --- x509.c 1 Aug 2025 17:29:30 -0000 1.117 +++ x509.c 27 Aug 2025 09:55:50 -0000 @@ -46,6 +46,7 @@ ASN1_OBJECT *aspa_oid; /* id-ct-ASPA */ ASN1_OBJECT *tak_oid; /* id-ct-SignedTAL */ ASN1_OBJECT *geofeed_oid; /* id-ct-geofeedCSVwithCRLF */ ASN1_OBJECT *spl_oid; /* id-ct-signedPrefixList */ +ASN1_OBJECT *ccr_oid; /* id-ct-rpkiCanonicalCacheRepresentation */ static const struct { const char *oid; @@ -122,6 +123,10 @@ static const struct { { .oid = "1.2.840.113549.1.9.16.1.51", .ptr = &spl_oid, + }, + { + .oid = "1.3.6.1.4.1.41948.825", + .ptr = &ccr_oid, }, };