diff --git a/usr.sbin/rpki-client/ccr.c b/usr.sbin/rpki-client/ccr.c index a78bbfb55ee..3c9f9ea59de 100644 --- a/usr.sbin/rpki-client/ccr.c +++ b/usr.sbin/rpki-client/ccr.c @@ -1,4 +1,4 @@ -/* $OpenBSD: ccr.c,v 1.5 2025/09/06 16:13:48 tb Exp $ */ +/* $OpenBSD: ccr.c,v 1.6 2025/09/09 08:23:24 job Exp $ */ /* * Copyright (c) 2025 Job Snijders * @@ -231,6 +231,41 @@ ASN1_ITEM_TEMPLATE_END(SubjectKeyIdentifiers); IMPLEMENT_ASN1_ENCODE_FUNCTIONS_fname(SubjectKeyIdentifiers, SubjectKeyIdentifiers, SubjectKeyIdentifiers); +static void +hash_asn1_item(ASN1_OCTET_STRING *astr, const ASN1_ITEM *it, void *val) +{ + unsigned char hash[SHA256_DIGEST_LENGTH]; + + if (!ASN1_item_digest(it, EVP_sha256(), val, hash, NULL)) + errx(1, "ASN1_item_digest"); + + if (!ASN1_OCTET_STRING_set(astr, hash, sizeof(hash))) + errx(1, "ASN1_STRING_set"); +} + +static char * +validate_asn1_hash(const char *fn, const char *descr, + const ASN1_OCTET_STRING *hash, const ASN1_ITEM *it, void *val) +{ + ASN1_OCTET_STRING *astr = NULL; + char *hex = NULL; + + if ((astr = ASN1_OCTET_STRING_new()) == NULL) + err(1, NULL); + + hash_asn1_item(astr, it, val); + + if (ASN1_OCTET_STRING_cmp(hash, astr) != 0) { + warnx("%s: corrupted %s state", fn, descr); + goto out; + } + + hex = hex_encode(hash->data, hash->length); + out: + ASN1_OCTET_STRING_free(astr); + return hex; +} + static void asn1int_set_seqnum(ASN1_INTEGER *aint, const char *seqnum) { @@ -291,16 +326,10 @@ append_cached_manifest(STACK_OF(ManifestRef) *mftrefs, struct ccr_mft *cm) errx(1, "sk_ManifestRef_push"); } -static void -hash_asn1_item(ASN1_OCTET_STRING *astr, const ASN1_ITEM *it, void *val) +static int +base64_encode_asn1str(const ASN1_OCTET_STRING *astr, char **out) { - unsigned char hash[SHA256_DIGEST_LENGTH]; - - if (!ASN1_item_digest(it, EVP_sha256(), val, hash, NULL)) - errx(1, "ASN1_item_digest"); - - if (!ASN1_OCTET_STRING_set(astr, hash, sizeof(hash))) - errx(1, "ASN1_STRING_set"); + return base64_encode(astr->data, astr->length, out) == 0; } static ManifestState * @@ -327,9 +356,8 @@ generate_manifeststate(struct validation_data *vd) hash_asn1_item(ms->hash, ASN1_ITEM_rptr(ManifestRefs), ms->mftrefs); - if (base64_encode(ms->hash->data, ms->hash->length, - &ccr->mfts_hash) == -1) - errx(1, "base64_encode"); + if (!base64_encode_asn1str(ms->hash, &ccr->mfts_hash)) + errx(1, "base64_encode_asn1str"); return ms; } @@ -374,6 +402,7 @@ append_cached_vrp(STACK_OF(ROAIPAddress) *addresses, struct vrp *vrp) static ROAPayloadState * generate_roapayloadstate(struct validation_data *vd) { + struct ccr *ccr = &vd->ccr; ROAPayloadState *vrps; struct vrp *prev, *vrp; ROAPayloadSet *rp; @@ -384,7 +413,7 @@ generate_roapayloadstate(struct validation_data *vd) 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"); @@ -421,9 +450,8 @@ generate_roapayloadstate(struct validation_data *vd) hash_asn1_item(vrps->hash, ASN1_ITEM_rptr(ROAPayloadSets), vrps->rps); - if (base64_encode(vrps->hash->data, vrps->hash->length, - &vd->ccr.vrps_hash) == -1) - errx(1, "base64_encode"); + if (!base64_encode_asn1str(vrps->hash, &ccr->vrps_hash)) + errx(1, "base64_encode_asn1str"); return vrps; } @@ -460,6 +488,7 @@ append_cached_aspa(STACK_OF(ASPAPayloadSet) *aps, struct vap *vap) static ASPAPayloadState * generate_aspapayloadstate(struct validation_data *vd) { + struct ccr *ccr = &vd->ccr; ASPAPayloadState *vaps; struct vap *vap; @@ -472,9 +501,8 @@ generate_aspapayloadstate(struct validation_data *vd) hash_asn1_item(vaps->hash, ASN1_ITEM_rptr(ASPAPayloadSets), vaps->aps); - if (base64_encode(vaps->hash->data, vaps->hash->length, - &vd->ccr.vaps_hash) == -1) - errx(1, "base64_encode"); + if (!base64_encode_asn1str(vaps->hash, &ccr->vaps_hash)) + errx(1, "base64_encode_asn1str"); return vaps; } @@ -723,3 +751,654 @@ output_ccr_der(FILE *out, struct validation_data *vd, struct stats *st) return 0; } + +static void +ccr_mft_free(struct ccr_mft *ccr_mft) +{ + if (ccr_mft == NULL) + return; + + free(ccr_mft->seqnum); + free(ccr_mft->sia); + free(ccr_mft); +} + +static void +ccr_mfts_free(struct ccr_mft_tree *mfts) +{ + struct ccr_mft *ccr_mft, *tmp_ccr_mft; + + RB_FOREACH_SAFE(ccr_mft, ccr_mft_tree, mfts, tmp_ccr_mft) { + RB_REMOVE(ccr_mft_tree, mfts, ccr_mft); + ccr_mft_free(ccr_mft); + } +} + +static void +ccr_vrps_free(struct ccr_vrp_tree *vrps) +{ + struct vrp *vrp, *tmp_vrp; + + RB_FOREACH_SAFE(vrp, ccr_vrp_tree, vrps, tmp_vrp) { + RB_REMOVE(ccr_vrp_tree, vrps, vrp); + free(vrp); + } +} + +static void +vap_free(struct vap *vap) +{ + if (vap == NULL) + return; + + free(vap->providers); + free(vap); +} + +static void +ccr_vaps_free(struct vap_tree *vaps) +{ + struct vap *vap, *tmp_vap; + + RB_FOREACH_SAFE(vap, vap_tree, vaps, tmp_vap) { + RB_REMOVE(vap_tree, vaps, vap); + vap_free(vap); + } +} + +static void +ccr_tas_free(struct ccr_tas_tree *tas) +{ + struct ccr_tas_ski *cts, *tmp_cts; + + RB_FOREACH_SAFE(cts, ccr_tas_tree, tas, tmp_cts) { + RB_REMOVE(ccr_tas_tree, tas, cts); + free(cts); + } +} + +void +ccr_free(struct ccr *ccr) +{ + if (ccr == NULL) + return; + + ccr_mfts_free(&ccr->mfts); + ccr_vrps_free(&ccr->vrps); + ccr_vaps_free(&ccr->vaps); + ccr_tas_free(&ccr->tas); + + free(ccr->mfts_hash); + free(ccr->vrps_hash); + free(ccr->vaps_hash); + free(ccr->tas_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 = NULL, *prev; + int i, refs_num; + const ACCESS_DESCRIPTION *ad; + int rc = 0; + uint64_t size = 0; + + refs_num = sk_ManifestRef_num(refs); + + RB_INIT(&ccr->mfts); + + prev = NULL; + for (i = 0; i < refs_num; i++) { + if ((ccr_mft = calloc(1, sizeof(*ccr_mft))) == NULL) + err(1, NULL); + + ref = sk_ManifestRef_value(refs, i); + + if (ref->hash->length != sizeof(ccr_mft->hash)) { + warnx("%s: manifest ref #%d corrupted", fn, i); + goto out; + } + memcpy(ccr_mft->hash, ref->hash->data, ref->hash->length); + + if (prev != NULL) { + if (ccr_mft_cmp(ccr_mft, prev) <= 0) { + warnx("%s: misordered ManifestRef", fn); + goto out; + } + } + + if (ref->aki->length != sizeof(ccr_mft->aki)) { + warnx("%s: manifest ref #%d corrupted", fn, i); + goto out; + } + memcpy(ccr_mft->aki, ref->aki->data, ref->aki->length); + + if (!ASN1_INTEGER_get_uint64(&size, ref->size)) { + warnx("%s: manifest ref #%d corrupted", fn, i); + goto out; + } + if (size < 1000 || 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 (!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); + goto out; + } + + prev = ccr_mft; + ccr_mft = NULL; + } + + rc = 1; + out: + ccr_mft_free(ccr_mft); + return rc; +} + +static int +parse_manifeststate(const char *fn, struct ccr *ccr, const ManifestState *state) +{ + int rc = 0; + + ccr->mfts_hash = validate_asn1_hash(fn, "ManifestState", state->hash, + ASN1_ITEM_rptr(ManifestRefs), state->mftrefs); + if (ccr->mfts_hash == NULL) + goto out; + + /* + * XXX: refactor into a x509_get_generalized_time() function. + */ + if (ASN1_STRING_length(state->mostRecentUpdate) != GENTIME_LENGTH) { + warnx("%s: mostRecentUpdate time format invalid", fn); + goto out; + } + if (!x509_get_time(state->mostRecentUpdate, &ccr->most_recent_update)) { + 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 *prev, *vrp = NULL; + uint64_t maxlen; + int addrs_num, i, rc = 0; + + if ((addrs_num = sk_ROAIPAddress_num(addrs)) <= 0) { + warnx("%s: missing ROAIPAddress", fn); + goto out; + } + + prev = NULL; + for (i = 0; i < addrs_num; i++) { + r = sk_ROAIPAddress_value(addrs, i); + + if ((vrp = calloc(1, sizeof(*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) <= 0) { + warnx("%s: misordered ROAIPAddressFamily", fn); + goto out; + } + } + + if ((RB_INSERT(ccr_vrp_tree, &ccr->vrps, vrp)) != NULL) { + warnx("%s: duplicate ROAIPAddress", fn); + goto out; + } + + prev = vrp; + vrp = NULL; + } + + rc = 1; + out: + free(vrp); + 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_num; + + ipb_num = sk_ROAIPAddressFamily_num(ipaddrblocks); + if (ipb_num != 1 && ipb_num != 2) { + warnx("%s: unexpected ipAddrBlocks count for AS %d", fn, asid); + goto out; + } + + for (i = 0; i < ipb_num; 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_num; + + rps_num = sk_ROAPayloadSet_num(rps); + + RB_INIT(&ccr->vrps); + + for (i = 0; i < rps_num; 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 = validate_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 *vap = NULL; + ASN1_INTEGER *aint; + uint32_t prev, provider; + int i, p_num, rc = 0; + + if ((p_num = sk_ASN1_INTEGER_num(providers)) <= 0) { + warnx("%s: AS %d ASPAPayloadSet providers missing", fn, asid); + goto out; + } + + if ((vap = calloc(1, sizeof(*vap))) == NULL) + err(1, NULL); + + vap->custasid = asid; + vap->num_providers = p_num; + + if ((vap->providers = calloc(p_num, sizeof(vap->providers[0]))) == NULL) + err(1, NULL); + + for (i = 0; i < p_num; 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 misordered providers", fn, + asid); + goto out; + } + } + + vap->providers[i] = provider; + prev = provider; + } + + if ((RB_INSERT(vap_tree, &ccr->vaps, vap)) != NULL) { + warnx("%s: duplicate ASPAPayloadSet", fn); + goto out; + } + vap = NULL; + + rc = 1; + out: + vap_free(vap); + return rc; +} + +static int +parse_aspa_payloads(const char *fn, struct ccr *ccr, + const STACK_OF(ASPAPayloadSet) *aps) +{ + ASPAPayloadSet *a; + uint32_t asid, prev; + int i, rc = 0, aps_num; + + aps_num = sk_ASPAPayloadSet_num(aps); + + RB_INIT(&ccr->vaps); + + for (i = 0; i < aps_num; i++) { + a = sk_ASPAPayloadSet_value(aps, i); + + if (!as_id_parse(a->asID, &asid)) { + warnx("%s: malformed asID in ASPAPayloadSet", fn); + goto out; + } + + if (i > 0) { + if (asid <= prev) { + warnx("%s: ASPAPayloadState misordered", fn); + goto out; + } + } + + if (!parse_aspa_providers(fn, ccr, asid, a->providers)) + goto out; + + prev = asid; + } + + rc = 1; + out: + return rc; +} + +static int +parse_aspastate(const char *fn, struct ccr *ccr, const ASPAPayloadState *state) +{ + int rc = 0; + + ccr->vaps_hash = validate_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; +} + +static int +parse_tas_skis(const char *fn, struct ccr *ccr, + const STACK_OF(SubjectKeyIdentifier) *skis) +{ + SubjectKeyIdentifier *ski; + struct ccr_tas_ski *cts = NULL, *prev; + int i, rc = 0, skis_num; + + if ((skis_num = sk_SubjectKeyIdentifier_num(skis)) <= 0) { + warnx("%s: missing TrustAnchorState SubjectKeyIdentifier", fn); + goto out; + } + + RB_INIT(&ccr->tas); + + prev = NULL; + for (i = 0; i < skis_num; i++) { + if ((cts = calloc(1, sizeof(*cts))) == NULL) + err(1, NULL); + + ski = sk_SubjectKeyIdentifier_value(skis, i); + + if (ski->length != sizeof(cts->keyid)) { + warnx("%s: TAS SKI #%d corrupted", fn, i); + goto out; + } + + memcpy(cts->keyid, ski->data, ski->length); + + if (prev != NULL) { + if (ccr_tas_ski_cmp(cts, prev) <= 0) { + warnx("%s: misordered TrustAnchorState", fn); + goto out; + } + } + + if (RB_INSERT(ccr_tas_tree, &ccr->tas, cts) != NULL) { + warnx("%s: trust anchor state corrupted", fn); + goto out; + } + + prev = cts; + cts = NULL; + } + + rc = 1; + out: + free(cts); + return rc; +} + +static int +parse_tastate(const char *fn, struct ccr *ccr, const TrustAnchorState *state) +{ + int rc = 0; + + ccr->tas_hash = validate_asn1_hash(fn, "TrustAnchorState", state->hash, + ASN1_ITEM_rptr(SubjectKeyIdentifiers), state->skis); + if (ccr->tas_hash == NULL) + goto out; + + if (!parse_tas_skis(fn, ccr, state->skis)) + 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; + CanonicalCacheRepresentation *ccr_asn1 = NULL; + struct ccr *ccr = NULL; + int nid, rc = 0; + + 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; + } + + der = ASN1_STRING_get0_data(ci->content); + len = ASN1_STRING_length(ci->content); + + oder = der; + ccr_asn1 = d2i_CanonicalCacheRepresentation(NULL, &der, len); + if (ccr_asn1 == NULL) { + warnx("%s: d2i_CanonicalCacheRepresentation failed", fn); + goto out; + } + if (der != oder + len) { + warnx("%s: %td bytes trailing garbage", fn, oder + len - der); + goto out; + } + + if (!valid_econtent_version(fn, ccr_asn1->version, 0)) + goto out; + + if ((nid = OBJ_obj2nid(ccr_asn1->hashAlg)) != NID_sha256) { + warnx("%s: hashAlg: want SHA256 object, have %s", fn, + nid2str(nid)); + 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 && ccr_asn1->vrps == NULL && + ccr_asn1->vaps == NULL && ccr_asn1->tas == NULL) { + warnx("%s: must contain at least one state component", 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; + } + + if (ccr_asn1->tas != NULL) { + if (!parse_tastate(fn, ccr, ccr_asn1->tas)) + goto out; + } + + rc = 1; + out: + CanonicalCacheRepresentation_free(ccr_asn1); + ContentInfo_free(ci); + + if (rc == 0) { + ccr_free(ccr); + ccr = NULL; + } + + return ccr; +} diff --git a/usr.sbin/rpki-client/encoding.c b/usr.sbin/rpki-client/encoding.c index 7c79d9815e2..96ac2baf076 100644 --- a/usr.sbin/rpki-client/encoding.c +++ b/usr.sbin/rpki-client/encoding.c @@ -1,4 +1,4 @@ -/* $OpenBSD: encoding.c,v 1.13 2022/05/15 15:00:53 deraadt Exp $ */ +/* $OpenBSD: encoding.c,v 1.14 2025/09/09 08:23:24 job Exp $ */ /* * Copyright (c) 2020 Claudio Jeker * @@ -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 || (!filemode && st.st_size > MAX_FILE_SIZE)) { errno = EFBIG; goto err; } diff --git a/usr.sbin/rpki-client/extern.h b/usr.sbin/rpki-client/extern.h index ae080b66d17..7d91a1370ff 100644 --- a/usr.sbin/rpki-client/extern.h +++ b/usr.sbin/rpki-client/extern.h @@ -1,4 +1,4 @@ -/* $OpenBSD: extern.h,v 1.261 2025/09/06 11:55:44 job Exp $ */ +/* $OpenBSD: extern.h,v 1.262 2025/09/09 08:23:24 job Exp $ */ /* * Copyright (c) 2019 Kristaps Dzonsons * @@ -202,6 +202,7 @@ enum rtype { RTYPE_TAK, RTYPE_GEOFEED, RTYPE_SPL, + RTYPE_CCR, }; enum location { @@ -487,11 +488,14 @@ RB_PROTOTYPE(ccr_tas_tree, ccr_tas_ski, entry, ccr_tas_ski_cmp); struct ccr { struct ccr_mft_tree mfts; struct ccr_vrp_tree vrps; + struct vap_tree vaps; /* only used in filemode */ struct ccr_tas_tree tas; char *mfts_hash; char *vrps_hash; char *vaps_hash; char *tas_hash; + time_t producedat; + time_t most_recent_update; unsigned char *der; size_t der_len; }; @@ -707,6 +711,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; @@ -1026,6 +1031,9 @@ int output_ccr_der(FILE *, struct validation_data *, struct stats *); /* * 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 ccr_insert_tas(struct ccr_tas_tree *, const struct cert *); diff --git a/usr.sbin/rpki-client/filemode.c b/usr.sbin/rpki-client/filemode.c index 3f780a35bdf..f278e15d10e 100644 --- a/usr.sbin/rpki-client/filemode.c +++ b/usr.sbin/rpki-client/filemode.c @@ -1,4 +1,4 @@ -/* $OpenBSD: filemode.c,v 1.67 2025/08/01 16:33:58 tb Exp $ */ +/* $OpenBSD: filemode.c,v 1.68 2025/09/09 08:23:24 job Exp $ */ /* * Copyright (c) 2019 Claudio Jeker * Copyright (c) 2019 Kristaps Dzonsons @@ -413,6 +413,7 @@ proc_parser_file(char *file, unsigned char *buf, size_t len) 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 char *buf, size_t len) 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 char *buf, size_t len) out: aspa_free(aspa); cert_free(cert); + ccr_free(ccr); crl_free(crl); gbr_free(gbr); geofeed_free(geofeed); diff --git a/usr.sbin/rpki-client/main.c b/usr.sbin/rpki-client/main.c index 5a1ee203fdc..3d2fc34f22e 100644 --- a/usr.sbin/rpki-client/main.c +++ b/usr.sbin/rpki-client/main.c @@ -1,4 +1,4 @@ -/* $OpenBSD: main.c,v 1.297 2025/09/06 11:55:44 job Exp $ */ +/* $OpenBSD: main.c,v 1.298 2025/09/09 08:23:24 job Exp $ */ /* * Copyright (c) 2021 Claudio Jeker * Copyright (c) 2019 Kristaps Dzonsons @@ -1016,7 +1016,7 @@ main(int argc, char *argv[]) const char *skiplistfile = NULL; struct rusage ru; struct timespec start_time, now_time; - struct validation_data vd; + struct validation_data vd = { 0 }; clock_gettime(CLOCK_MONOTONIC, &start_time); diff --git a/usr.sbin/rpki-client/mft.c b/usr.sbin/rpki-client/mft.c index 9e568e369b4..13e8408122e 100644 --- a/usr.sbin/rpki-client/mft.c +++ b/usr.sbin/rpki-client/mft.c @@ -1,4 +1,4 @@ -/* $OpenBSD: mft.c,v 1.130 2025/08/24 12:17:12 tb Exp $ */ +/* $OpenBSD: mft.c,v 1.131 2025/09/09 08:23:24 job Exp $ */ /* * Copyright (c) 2022 Theo Buehler * Copyright (c) 2019 Kristaps Dzonsons @@ -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; } diff --git a/usr.sbin/rpki-client/print.c b/usr.sbin/rpki-client/print.c index 962221a3827..b7cce9f5674 100644 --- a/usr.sbin/rpki-client/print.c +++ b/usr.sbin/rpki-client/print.c @@ -1,4 +1,4 @@ -/* $OpenBSD: print.c,v 1.65 2025/07/20 14:23:44 tb Exp $ */ +/* $OpenBSD: print.c,v 1.66 2025/09/09 08:23:24 job Exp $ */ /* * Copyright (c) 2021 Claudio Jeker * Copyright (c) 2019 Kristaps Dzonsons @@ -883,3 +883,216 @@ geofeed_print(const struct cert *c, const struct geofeed *p) if (outformats & FORMAT_JSON) json_do_end(); } + +static void +print_ccr_mftstate(struct ccr *ccr) +{ + char *aki, *hash; + struct ccr_mft *ccr_mft; + + 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->most_recent_update); + 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->most_recent_update)); + 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 { + printf("%26shash:%s size:%zu aki:%s seqnum:%s sia:%s\n", + "", hash, ccr_mft->size, aki, ccr_mft->seqnum, + ccr_mft->sia); + } + + free(aki); + free(hash); + } + if (outformats & FORMAT_JSON) { + json_do_end(); + 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(); + 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(); + } +} + +static void +print_ccr_tastate(struct ccr *ccr) +{ + char *hash, *ski; + struct ccr_tas_ski *cts; + int i = 0; + + if (base64_encode(ccr->tas_hash, SHA256_DIGEST_LENGTH, &hash) == -1) + errx(1, "base64_encode"); + + if (outformats & FORMAT_JSON) { + json_do_object("trustanchor_state", 0); + json_do_string("hash", hash); + json_do_array("skis"); + } else { + printf("Trust anchor state hash: %s\n", hash); + printf("Trust anchor keyids: "); + } + + free(hash); + + RB_FOREACH(cts, ccr_tas_tree, &ccr->tas) { + ski = hex_encode(cts->keyid, sizeof(cts->keyid)); + + if (outformats & FORMAT_JSON) { + json_do_string("ski", ski); + } else { + if (++i > 1) + printf(", "); + printf("%s", ski); + } + + free(ski); + } + + if (outformats & FORMAT_JSON) { + json_do_end(); + json_do_end(); + } else { + printf("\n"); + } +} + +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); + + if (ccr->tas_hash != NULL) + print_ccr_tastate(ccr); +} diff --git a/usr.sbin/rpki-client/roa.c b/usr.sbin/rpki-client/roa.c index ed0147a6001..90adfff34f2 100644 --- a/usr.sbin/rpki-client/roa.c +++ b/usr.sbin/rpki-client/roa.c @@ -1,4 +1,4 @@ -/* $OpenBSD: roa.c,v 1.86 2025/08/24 11:52:20 tb Exp $ */ +/* $OpenBSD: roa.c,v 1.87 2025/09/09 08:23:24 job Exp $ */ /* * Copyright (c) 2022 Theo Buehler * Copyright (c) 2019 Kristaps Dzonsons @@ -95,6 +95,11 @@ roa_parse_econtent(const char *fn, struct roa *roa, const unsigned char *d, if (!valid_econtent_version(fn, roa_asn1->version, 0)) goto out; + /* + * XXX: from here until the function end should be refactored + * to deduplicate similar code in ccr.c. + */ + if (!as_id_parse(roa_asn1->asid, &roa->asid)) { warnx("%s: RFC 9582 section 4.2: asID: " "malformed AS identifier", fn); diff --git a/usr.sbin/rpki-client/rpki-asn1.h b/usr.sbin/rpki-client/rpki-asn1.h index da7d17576e1..e3be8da91fc 100644 --- a/usr.sbin/rpki-client/rpki-asn1.h +++ b/usr.sbin/rpki-client/rpki-asn1.h @@ -1,4 +1,4 @@ -/* $OpenBSD: rpki-asn1.h,v 1.3 2025/09/06 11:55:44 job Exp $ */ +/* $OpenBSD: rpki-asn1.h,v 1.4 2025/09/09 08:23:24 job Exp $ */ /* * Copyright (c) 2025 Job Snijders * Copyright (c) 2025 Theo Buehler @@ -24,6 +24,8 @@ #include #include +#define GENTIME_LENGTH 15 + /* * Autonomous System Provider Authorization (ASPA) * reference: draft-ietf-sidrops-aspa-profile @@ -69,6 +71,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); @@ -95,6 +98,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); @@ -120,6 +124,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); @@ -142,8 +147,11 @@ DECLARE_ASN1_FUNCTIONS(SubjectKeyIdentifier); DECLARE_STACK_OF(SubjectKeyIdentifier); #ifndef DEFINE_STACK_OF +#define sk_SubjectKeyIdentifier_num(st) SKM_sk_num(SubjectKeyIdentifier, (st)) #define sk_SubjectKeyIdentifier_push(st, i) \ SKM_sk_push(SubjectKeyIdentifier, (st), (i)) +#define sk_SubjectKeyIdentifier_value(st, i) \ + SKM_sk_value(SubjectKeyIdentifier, (st), (i)) #endif typedef STACK_OF(SubjectKeyIdentifier) SubjectKeyIdentifiers; diff --git a/usr.sbin/rpki-client/x509.c b/usr.sbin/rpki-client/x509.c index 641ae0c27b0..fbf30e8cd7f 100644 --- a/usr.sbin/rpki-client/x509.c +++ b/usr.sbin/rpki-client/x509.c @@ -1,4 +1,4 @@ -/* $OpenBSD: x509.c,v 1.117 2025/08/01 17:29:30 tb Exp $ */ +/* $OpenBSD: x509.c,v 1.118 2025/09/09 08:23:24 job Exp $ */ /* * Copyright (c) 2022 Theo Buehler * Copyright (c) 2021 Claudio Jeker @@ -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; /* CanonicalCacheRepresentation PEN OID */ static const struct { const char *oid; @@ -123,6 +124,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, + }, }; void