Dear all, The below changeset extends rpki-client(8) to validate and emit Autonomous System Provider Authorizations (ASPAs) in JSON format for consumption in other routing stacks.
The ASPA CMS protected content type is specified here: https://datatracker.ietf.org/doc/html/draft-ietf-sidrops-aspa-profile How to verify BGP UPDATES using ASPA objects is described here: https://datatracker.ietf.org/doc/html/draft-ietf-sidrops-aspa-verification (Note: aspa-profile and aspa-verification really are two different beasts) A test suite to generate conforming and non-conforming ASPA objects is available at: https://github.com/benmaddison/rpki-aspa-test-data Another testbed was outlined here: https://mailarchive.ietf.org/arch/msg/sidrops/DvRqMbOXkhiBDdUCTJU89L4q6LY The rpki-client(8) '-j' (/var/db/rpki-client/json) output is formatted such that transformation into draft-ietf-sidrops-8210bis § 5.12 RPKI-To-Router PDUs should be easy. However, for future direct disk-to-bgpd(8) dissemination of ASPA objects; we probably can come up with something more efficient :-) Example output from "rpki-client -f": File: rpki.example.net/rpki/TA/ca-case-multi-afi-true/2338bd30cba1ff30fe3cf930824f39b45569d458de356729cb0121400ee3616a.asa Hash identifier: P+IG54iYS6VLlZmnIacSks0bZwHxItkIHPt5YjI4y58= Subject key identifier: CB:B8:19:D8:E0:FD:8E:0E:10:83:D6:11:6B:2C:CD:92:28:8F:F8:C9 Certificate serial: 01 Authority key identifier: 3C:E8:78:93:CE:9C:7F:41:53:0D:D0:E8:BB:CA:B0:03:7F:69:43:74 Authority info access: rsync://rpki.example.net/rpki/TA/ca-case-multi-afi-true.cer Customer AS: 65004 Provider Set: 1: AS: 65005 2: AS: 65006 (IPv4 only) 3: AS: 65007 (IPv6 only) Validation: OK OK? Feedback? Kind regards, Job Index: Makefile =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/Makefile,v retrieving revision 1.25 diff -u -p -r1.25 Makefile --- Makefile 9 May 2022 17:02:34 -0000 1.25 +++ Makefile 13 Aug 2022 00:24:24 -0000 @@ -1,8 +1,8 @@ # $OpenBSD: Makefile,v 1.25 2022/05/09 17:02:34 job Exp $ PROG= rpki-client -SRCS= as.c cert.c cms.c crl.c encoding.c filemode.c gbr.c http.c io.c ip.c \ - log.c main.c mft.c mkdir.c output.c output-bgpd.c output-bird.c \ +SRCS= as.c aspa.c cert.c cms.c crl.c encoding.c filemode.c gbr.c http.c io.c \ + ip.c log.c main.c mft.c mkdir.c output.c output-bgpd.c output-bird.c \ output-csv.c output-json.c parser.c print.c repo.c roa.c rrdp.c \ rrdp_delta.c rrdp_notification.c rrdp_snapshot.c rrdp_util.c \ rsc.c rsync.c tal.c validate.c x509.c Index: aspa.c =================================================================== RCS file: aspa.c diff -N aspa.c --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ aspa.c 13 Aug 2022 00:24:24 -0000 @@ -0,0 +1,447 @@ +/* $OpenBSD: aspa.c,v 1.16 2022/05/11 21:19:06 job Exp $ */ +/* + * Copyright (c) 2022 Job Snijders <j...@fastly.com> + * + * 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 <assert.h> +#include <err.h> +#include <stdarg.h> +#include <stdint.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 <openssl/x509.h> + +#include "extern.h" + +/* + * Parse results and data of the ASPA object. + */ +struct parse { + const char *fn; /* ASPA file name */ + struct aspa *res; /* results */ +}; + +extern ASN1_OBJECT *aspa_oid; + +/* + * Types and templates for ASPA eContent draft-ietf-sidrops-aspa-profile-08 + */ + +typedef struct { + ASN1_INTEGER *providerASID; + ASN1_OCTET_STRING *afiLimit; +} ProviderAS; + +DECLARE_STACK_OF(ProviderAS); + +#ifndef DEFINE_STACK_OF +#define sk_ProviderAS_num(sk) SKM_sk_num(ProviderAS, (sk)) +#define sk_ProviderAS_value(sk, i) SKM_sk_value(ProviderAS, (sk), (i)) +#endif + +ASN1_SEQUENCE(ProviderAS) = { + ASN1_SIMPLE(ProviderAS, providerASID, ASN1_INTEGER), + ASN1_OPT(ProviderAS, afiLimit, ASN1_OCTET_STRING), +} ASN1_SEQUENCE_END(ProviderAS); + +typedef struct { + ASN1_INTEGER *version; + ASN1_INTEGER *customerASID; + STACK_OF(ProviderAS) *providers; +} ASProviderAttestation; + +ASN1_SEQUENCE(ASProviderAttestation) = { + ASN1_IMP_OPT(ASProviderAttestation, version, ASN1_INTEGER, 0), + ASN1_SIMPLE(ASProviderAttestation, customerASID, ASN1_INTEGER), + ASN1_SEQUENCE_OF(ASProviderAttestation, providers, ProviderAS), +} ASN1_SEQUENCE_END(ASProviderAttestation); + +DECLARE_ASN1_FUNCTIONS(ASProviderAttestation); +IMPLEMENT_ASN1_FUNCTIONS(ASProviderAttestation); + +/* + * Parse the ProviderASSet sequence. + * Return zero on failure, non-zero on success. + */ +static int +aspa_parse_providers(struct parse *p, const STACK_OF(ProviderAS) *providers) +{ + ProviderAS *pa; + struct aspa_provider aspa_p; + size_t providersz, i, j; + + memset(&aspa_p, 0, sizeof(struct aspa_provider)); + + if ((providersz = sk_ProviderAS_num(providers)) == 0) { + warnx("%s: ASPA: ProviderASSet needs at least one entry", + p->fn); + return 0; + } + + if (providersz >= MAX_ASPA_PROVIDERS) { + warnx("%s: ASPA: too many providers (more than %d)", p->fn, + MAX_ASPA_PROVIDERS); + return 0; + } + + p->res->providers = calloc(providersz, sizeof(struct aspa_provider)); + if (p->res->providers == NULL) + err(1, NULL); + + for (i = 0; i < providersz; i++) { + pa = sk_ProviderAS_value(providers, i); + + if (!as_id_parse(pa->providerASID, &aspa_p.as)) { + warnx("%s: ASPA: malformed ProviderAS", p->fn); + return 0; + } + + if (p->res->custasid == aspa_p.as) { + warnx("%s: ASPA: CustomerASID can't also be Provider", + p->fn); + return 0; + } + + if (i > 0) + if (p->res->providers[i - 1].as > aspa_p.as) { + warnx("%s: ASPA: invalid ProviderASSet order", + p->fn); + return 0; + } + + for (j = 0; j < i; j++) { + if (p->res->providers[j].as == aspa_p.as) { + warnx("%s: ASPA: duplicate ProviderAS", p->fn); + return 0; + } + } + + if (pa->afiLimit == NULL) + aspa_p.afi = AFI_BOTH; + else if (!ip_addr_afi_parse(p->fn, pa->afiLimit, &aspa_p.afi)) { + warnx("%s: ASPA: invalid afiLimit", p->fn); + return 0; + } + + p->res->providers[p->res->providersz++] = aspa_p; + } + + return 1; +} + +/* + * Parse the eContent of an ASPA file. + * Returns zero on failure, non-zero on success. + */ +static int +aspa_parse_econtent(const unsigned char *d, size_t dsz, struct parse *p) +{ + ASProviderAttestation *aspa; + int rc = 0; + + if ((aspa = d2i_ASProviderAttestation(NULL, &d, dsz)) == NULL) { + cryptowarnx("%s: ASPA: failed to parse ASProviderAttestation", + p->fn); + goto out; + } + + if (!valid_econtent_version(p->fn, aspa->version)) + goto out; + + if (aspa->customerASID == NULL) { + warnx("%s: ASPA: customerASID must be present", p->fn); + goto out; + } + + if (!as_id_parse(aspa->customerASID, &p->res->custasid)) { + warnx("%s: malformed CustomerASID", p->fn); + goto out; + } + + if (!aspa_parse_providers(p, aspa->providers)) + goto out; + + rc = 1; + out: + ASProviderAttestation_free(aspa); + return rc; +} + +/* + * Parse a full ASPA file. + * Returns the payload or NULL if the file was malformed. + */ +struct aspa * +aspa_parse(X509 **x509, const char *fn, const unsigned char *der, size_t len) +{ + struct parse p; + size_t cmsz; + unsigned char *cms; + const ASN1_TIME *at; + int rc = 0; + + memset(&p, 0, sizeof(struct parse)); + p.fn = fn; + + cms = cms_parse_validate(x509, fn, der, len, aspa_oid, &cmsz); + if (cms == NULL) + return NULL; + + if ((p.res = calloc(1, sizeof(*p.res))) == NULL) + err(1, NULL); + + if (!x509_get_aia(*x509, fn, &p.res->aia)) + goto out; + if (!x509_get_aki(*x509, fn, &p.res->aki)) + goto out; + if (!x509_get_ski(*x509, fn, &p.res->ski)) + goto out; + if (p.res->aia == NULL || p.res->aki == NULL || p.res->ski == NULL) { + warnx("%s: RFC 6487 section 4.8: " + "missing AIA, AKI or SKI X509 extension", fn); + goto out; + } + + if (X509_get_ext_by_NID(*x509, NID_sbgp_ipAddrBlock, -1) != -1) { + warnx("%s: superfluous IP Resources extension present", fn); + goto out; + } + + at = X509_get0_notAfter(*x509); + if (at == NULL) { + warnx("%s: X509_get0_notAfter failed", fn); + goto out; + } + if (x509_get_time(at, &p.res->expires) == -1) { + warnx("%s: ASN1_time_parse failed", fn); + goto out; + } + + if (!aspa_parse_econtent(cms, cmsz, &p)) + goto out; + + rc = 1; + out: + if (rc == 0) { + aspa_free(p.res); + p.res = NULL; + X509_free(*x509); + *x509 = NULL; + } + free(cms); + return p.res; +} + +/* + * Free a ASPA pointer. + * Safe to call with NULL. + */ +void +aspa_free(struct aspa *p) +{ + if (p == NULL) + return; + + free(p->aia); + free(p->aki); + free(p->ski); + free(p->providers); + free(p); +} + +/* + * Serialise parsed ASPA content. + * See aspa_read() for the reader on the other side. + */ +void +aspa_buffer(struct ibuf *b, const struct aspa *p) +{ + io_simple_buffer(b, &p->valid, sizeof(p->valid)); + io_simple_buffer(b, &p->custasid, sizeof(p->custasid)); + io_simple_buffer(b, &p->expires, sizeof(p->expires)); + + io_simple_buffer(b, &p->providersz, sizeof(size_t)); + io_simple_buffer(b, p->providers, + p->providersz * sizeof(p->providers[0])); + + io_str_buffer(b, p->aia); + io_str_buffer(b, p->aki); + io_str_buffer(b, p->ski); +} + +/* + * Read parsed ASPA content from descriptor. + * See aspa_buffer() for writer. + * Result must be passed to aspa_free(). + */ +struct aspa * +aspa_read(struct ibuf *b) +{ + struct aspa *p; + + if ((p = calloc(1, sizeof(struct aspa))) == NULL) + err(1, NULL); + + io_read_buf(b, &p->valid, sizeof(p->valid)); + io_read_buf(b, &p->custasid, sizeof(p->custasid)); + io_read_buf(b, &p->expires, sizeof(p->expires)); + + io_read_buf(b, &p->providersz, sizeof(size_t)); + if ((p->providers = calloc(p->providersz, + sizeof(struct aspa_provider))) == NULL) + err(1, NULL); + io_read_buf(b, p->providers, p->providersz * sizeof(p->providers[0])); + + io_read_str(b, &p->aia); + io_read_str(b, &p->aki); + io_read_str(b, &p->ski); + assert(p->aia && p->aki && p->ski); + + return p; +} + +/* + * draft-ietf-sidrops-8210bis § 5.12 states: + * + * "The router MUST see at most one ASPA for a given AFI from a cache for + * a particular Customer ASID active at any time. As a number of conditions + * in the global RPKI may present multiple valid ASPA RPKI records for a + * single customer to a particular RP cache, this places a burden on the + * cache to form the union of multiple ASPA records it has received from + * the global RPKI into one RPKI-To-Router (RTR) ASPA PDU." + * + * The above described 'burden' (which is specific to RTR) is resolved in + * insert_vap() and aspa_insert_vaps() functions below. + * + * XXX: for bgpd(8), ASPA config injection (via /var/db/rpki-client/openbgpd) + * we probably want to undo the 'burden solving' and compress into implicit + * AFIs. + */ + +/* + * If the CustomerASID (CAS) showed up before, append the ProviderAS (PAS); + * otherwise create a new entry in the RB tree. + * Ensure there are no duplicates in the 'providers' array. + * Always compare 'expires': use the soonest expiration moment. + */ +static int +insert_vap(struct vap_tree *tree, uint32_t cas, uint32_t pas, time_t expires, + enum afi afi) +{ + struct vap *v, *found; + int append; + size_t i; + + if ((v = malloc(sizeof(*v))) == NULL) + err(1, NULL); + v->afi = afi; + v->custasid = cas; + v->expires = expires; + + if ((found = RB_INSERT(vap_tree, tree, v)) != NULL) { + if (found->expires > expires) + found->expires = expires; + + append = 1; + for (i = 0; i < found->providersz; i++) + if (found->providers[i] == pas) { + append = 0; + break; + } + if (append) { + found->providers = reallocarray(found->providers, + found->providersz + 1, sizeof(uint32_t)); + if (found->providers == NULL) + err(1, NULL); + found->providers[found->providersz++] = pas; + } + + free(v); + } else { + if ((v->providers = malloc(sizeof(uint32_t))) == NULL) + err(1, NULL); + v->providers[0] = pas; + v->providersz = 1; + } + + return 1; +} + +/* + * Add each ProviderAS entry into the Validated ASPA Providers (VAP) tree. + * Updates "vaps" to be the total number of VAPs, and "uniqs" to be the + * pre-'AFI explosion' deduplicated count. + */ +void +aspa_insert_vaps(struct vap_tree *tree, struct aspa *aspa, size_t *vaps, + size_t *uniqs) +{ + size_t i; + uint32_t cas, pas; + time_t expires; + + cas = aspa->custasid; + expires = aspa->expires; + + for (i = 0; i < aspa->providersz; i++) { + pas = aspa->providers[i].as; + + switch (aspa->providers[i].afi) { + case AFI_IPV4: + if (!insert_vap(tree, cas, pas, expires, AFI_IPV4)) + err(1, NULL); + (*vaps)++; + break; + case AFI_IPV6: + if (!insert_vap(tree, cas, pas, expires, AFI_IPV6)) + err(1, NULL); + (*vaps)++; + break; + case AFI_BOTH: + if (!insert_vap(tree, cas, pas, expires, AFI_IPV4)) + err(1, NULL); + (*vaps)++; + if (!insert_vap(tree, cas, pas, expires, AFI_IPV6)) + err(1, NULL); + (*vaps)++; + break; + } + (*uniqs)++; + } +} + +static inline int +vapcmp(struct vap *a, struct vap *b) +{ + if (a->afi > b->afi) + return 1; + if (a->afi < b->afi) + return -1; + + if (a->custasid > b->custasid) + return 1; + if (a->custasid < b->custasid) + return -1; + + return 0; +} + +RB_GENERATE(vap_tree, vap, entry, vapcmp); Index: extern.h =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/extern.h,v retrieving revision 1.147 diff -u -p -r1.147 extern.h --- extern.h 10 Aug 2022 10:27:03 -0000 1.147 +++ extern.h 13 Aug 2022 00:24:24 -0000 @@ -74,7 +74,8 @@ struct cert_as { */ enum afi { AFI_IPV4 = 1, - AFI_IPV6 = 2 + AFI_IPV6 = 2, + AFI_BOTH = 3 }; /* @@ -283,6 +284,45 @@ struct gbr { char *ski; /* SKI */ }; +struct aspa_provider { + uint32_t as; + enum afi afi; +}; + +/* + * A single ASPA record + */ +struct aspa { + int valid; /* contained in parent auth */ + int talid; /* TAL the ASPA is chained up to */ + char *aia; /* AIA */ + char *aki; /* AKI */ + char *ski; /* SKI */ + uint32_t custasid; /* the customerASID */ + struct aspa_provider *providers; /* the providers */ + size_t providersz; /* number of providers */ + time_t expires; /* NotAfter of the ASPA EE cert */ +}; + +/* + * A Validated ASPA Payload (VAP) tree element. + * To ease transformation, this struct mimicks ASPA RTR PDU structure. + */ +struct vap { + RB_ENTRY(vap) entry; + enum afi afi; + uint32_t custasid; + uint32_t *providers; + size_t providersz; + time_t expires; +}; + +/* + * Tree of VAPs sorted by afi, custasid, and provideras. + */ +RB_HEAD(vap_tree, vap); +RB_PROTOTYPE(vap_tree, vap, entry, vapcmp); + /* * A single VRP element (including ASID) */ @@ -433,6 +473,11 @@ struct stats { size_t rrdp_fails; /* failed rrdp repositories */ size_t crls; /* revocation lists */ size_t gbrs; /* ghostbuster records */ + size_t aspas; /* ASPA objects */ + size_t aspas_fail; /* ASPA objects failing syntactic parse */ + size_t aspas_invalid; /* ASPAs with invalid customerASID */ + size_t vaps; /* total number of Validated ASPA Payloads */ + size_t vaps_uniqs; /* total number of unique VAPs */ size_t vrps; /* total number of vrps */ size_t uniqs; /* number of unique vrps */ size_t del_files; /* number of files removed in cleanup */ @@ -496,6 +541,14 @@ void rsc_free(struct rsc *); struct rsc *rsc_parse(X509 **, const char *, const unsigned char *, size_t); +void aspa_buffer(struct ibuf *, const struct aspa *); +void aspa_free(struct aspa *); +void aspa_insert_vaps(struct vap_tree *, struct aspa *, size_t *, + size_t *); +struct aspa *aspa_parse(X509 **, const char *, const unsigned char *, + size_t); +struct aspa *aspa_read(struct ibuf *); + /* crl.c */ struct crl *crl_parse(const char *, const unsigned char *, size_t); struct crl *crl_get(struct crl_tree *, const struct auth *); @@ -518,6 +571,7 @@ int valid_origin(const char *, const c int valid_x509(char *, X509_STORE_CTX *, X509 *, struct auth *, struct crl *, int); int valid_rsc(const char *, struct auth *, struct rsc *); +int valid_aspa(const char *, struct auth *, struct aspa *); int valid_econtent_version(const char *, const ASN1_INTEGER *); /* Working with CMS. */ @@ -664,6 +718,7 @@ void mft_print(const X509 *, const str void roa_print(const X509 *, const struct roa *); void gbr_print(const X509 *, const struct gbr *); void rsc_print(const X509 *, const struct rsc *); +void aspa_print(const X509 *, const struct aspa *); /* Output! */ @@ -674,20 +729,20 @@ extern int outformats; #define FORMAT_JSON 0x08 int outputfiles(struct vrp_tree *v, struct brk_tree *b, - struct stats *); + struct vap_tree *, struct stats *); int outputheader(FILE *, struct stats *); int output_bgpd(FILE *, struct vrp_tree *, struct brk_tree *, - struct stats *); + struct vap_tree *, struct stats *); int output_bird1v4(FILE *, struct vrp_tree *, struct brk_tree *, - struct stats *); + struct vap_tree *, struct stats *); int output_bird1v6(FILE *, struct vrp_tree *, struct brk_tree *, - struct stats *); + struct vap_tree *, struct stats *); int output_bird2(FILE *, struct vrp_tree *, struct brk_tree *, - struct stats *); + struct vap_tree *, struct stats *); int output_csv(FILE *, struct vrp_tree *, struct brk_tree *, - struct stats *); + struct vap_tree *, struct stats *); int output_json(FILE *, struct vrp_tree *, struct brk_tree *, - struct stats *); + struct vap_tree *, struct stats *); void logx(const char *fmt, ...) __attribute__((format(printf, 1, 2))); @@ -719,6 +774,9 @@ int mkpathat(int, const char *); /* Maximum number of FileAndHash entries per manifest. */ #define MAX_MANIFEST_ENTRIES 100000 + +/* Maximum number of Providers per ASPA object. */ +#define MAX_ASPA_PROVIDERS 10000 /* Maximum depth of the RPKI tree. */ #define MAX_CERT_DEPTH 12 Index: filemode.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/filemode.c,v retrieving revision 1.7 diff -u -p -r1.7 filemode.c --- filemode.c 11 May 2022 14:42:01 -0000 1.7 +++ filemode.c 13 Aug 2022 00:24:24 -0000 @@ -265,6 +265,7 @@ proc_parser_file(char *file, unsigned ch struct gbr *gbr = NULL; struct tal *tal = NULL; struct rsc *rsc = NULL; + struct aspa *aspa = NULL; char *aia = NULL, *aki = NULL; char filehash[SHA256_DIGEST_LENGTH]; char *hash; @@ -366,6 +367,14 @@ proc_parser_file(char *file, unsigned ch aia = rsc->aia; aki = rsc->aki; break; + case RTYPE_ASPA: + aspa = aspa_parse(&x509, file, buf, len); + if (aspa == NULL) + break; + aspa_print(x509, aspa); + aia = aspa->aia; + aki = aspa->aki; + break; default: printf("%s: unsupported file type\n", file); break; @@ -391,10 +400,19 @@ proc_parser_file(char *file, unsigned ch c = crl_get(&crlt, a); if ((status = valid_x509(file, ctx, x509, a, c, 0))) { - if (type == RTYPE_ROA) + switch (type) { + case RTYPE_ROA: status = valid_roa(file, a, roa); - else if (type == RTYPE_RSC) + break; + case RTYPE_RSC: status = valid_rsc(file, a, rsc); + break; + case RTYPE_ASPA: + status = valid_aspa(file, a, aspa); + break; + default: + break; + } } if (status) printf("OK"); @@ -431,6 +449,7 @@ proc_parser_file(char *file, unsigned ch roa_free(roa); gbr_free(gbr); tal_free(tal); + aspa_free(aspa); } /* Index: main.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/main.c,v retrieving revision 1.209 diff -u -p -r1.209 main.c --- main.c 4 Aug 2022 13:44:07 -0000 1.209 +++ main.c 13 Aug 2022 00:24:25 -0000 @@ -1,4 +1,4 @@ -/* $OpenBSD: main.c,v 1.209 2022/08/04 13:44:07 claudio Exp $ */ +/* $OpenBSD: main.c,v 1.208 2022/06/27 10:18:27 job Exp $ */ /* * Copyright (c) 2021 Claudio Jeker <clau...@openbsd.org> * Copyright (c) 2019 Kristaps Dzonsons <krist...@bsd.lv> @@ -475,13 +475,14 @@ queue_add_from_cert(const struct cert *c */ static void entity_process(struct ibuf *b, struct stats *st, struct vrp_tree *tree, - struct brk_tree *brktree) + struct brk_tree *brktree, struct vap_tree *vaptree) { enum rtype type; struct tal *tal; struct cert *cert; struct mft *mft; struct roa *roa; + struct aspa *aspa; char *file; int c; @@ -568,6 +569,21 @@ entity_process(struct ibuf *b, struct st break; case RTYPE_FILE: break; + case RTYPE_ASPA: + st->aspas++; + io_read_buf(b, &c, sizeof(c)); + if (c == 0) { + st->aspas_fail++; + break; + } + aspa = aspa_read(b); + if (aspa->valid) + aspa_insert_vaps(vaptree, aspa, &st->vaps, + &st->vaps_uniqs); + else + st->aspas_invalid++; + aspa_free(aspa); + break; default: errx(1, "unknown entity type %d", type); } @@ -793,6 +809,7 @@ main(int argc, char *argv[]) const char *skiplistfile = NULL; struct vrp_tree vrps = RB_INITIALIZER(&vrps); struct brk_tree brks = RB_INITIALIZER(&brks); + struct vap_tree vaps = RB_INITIALIZER(&vaps); struct rusage ru; struct timeval start_time, now_time; @@ -1006,7 +1023,8 @@ main(int argc, char *argv[]) signal(SIGALRM, suicide); } - if (pledge("stdio rpath wpath cpath fattr sendfd unveil", NULL) == -1) + /* TODO unveil cachedir and outputdir, no other access allowed */ + if (pledge("stdio rpath wpath cpath fattr sendfd", NULL) == -1) err(1, "pledge"); msgbuf_init(&procq); @@ -1047,17 +1065,7 @@ main(int argc, char *argv[]) if (filemode) { while (*argv != NULL) queue_add_file(*argv++, RTYPE_FILE, 0); - - if (unveil(cachedir, "r") == -1) - err(1, "unveil cachedir"); - } else { - if (unveil(outputdir, "rwc") == -1) - err(1, "unveil outputdir"); - if (unveil(cachedir, "rwc") == -1) - err(1, "unveil cachedir"); } - if (pledge("stdio rpath wpath cpath fattr sendfd", NULL) == -1) - err(1, "unveil"); /* change working directory to the cache directory */ if (fchdir(cachefd) == -1) @@ -1159,7 +1167,7 @@ main(int argc, char *argv[]) if ((pfd[0].revents & POLLIN)) { b = io_buf_read(proc, &procbuf); if (b != NULL) { - entity_process(b, &stats, &vrps, &brks); + entity_process(b, &stats, &vrps, &brks, &vaps); ibuf_free(b); } } @@ -1242,7 +1250,7 @@ main(int argc, char *argv[]) if (fchdir(outdirfd) == -1) err(1, "fchdir output dir"); - if (outputfiles(&vrps, &brks, &stats)) + if (outputfiles(&vrps, &brks, &vaps, &stats)) rc = 1; printf("Processing time %lld seconds " @@ -1253,6 +1261,8 @@ main(int argc, char *argv[]) printf("Skiplist entries: %zu\n", stats.skiplistentries); printf("Route Origin Authorizations: %zu (%zu failed parse, %zu invalid)\n", stats.roas, stats.roas_fail, stats.roas_invalid); + printf("AS Provider Attestations: %zu (%zu failed parse, %zu invalid)\n", + stats.aspas, stats.aspas_fail, stats.aspas_invalid); printf("BGPsec Router Certificates: %zu\n", stats.brks); printf("Certificates: %zu (%zu invalid)\n", stats.certs, stats.certs_fail); @@ -1266,6 +1276,7 @@ main(int argc, char *argv[]) printf("Cleanup: removed %zu files, %zu directories, %zu superfluous\n", stats.del_files, stats.del_dirs, stats.extra_files); printf("VRP Entries: %zu (%zu unique)\n", stats.vrps, stats.uniqs); + printf("VAP Entries: %zu (%zu unique)\n", stats.vaps, stats.vaps_uniqs); /* Memory cleanup. */ repo_free(); Index: output-bgpd.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/output-bgpd.c,v retrieving revision 1.23 diff -u -p -r1.23 output-bgpd.c --- output-bgpd.c 11 Oct 2021 16:50:03 -0000 1.23 +++ output-bgpd.c 13 Aug 2022 00:24:25 -0000 @@ -21,7 +21,7 @@ int output_bgpd(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks, - struct stats *st) + struct vap_tree *vaps, struct stats *st) { struct vrp *v; Index: output-bird.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/output-bird.c,v retrieving revision 1.14 diff -u -p -r1.14 output-bird.c --- output-bird.c 15 May 2022 16:43:34 -0000 1.14 +++ output-bird.c 13 Aug 2022 00:24:25 -0000 @@ -22,7 +22,7 @@ int output_bird1v4(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks, - struct stats *st) + struct vap_tree *vaps, struct stats *st) { extern const char *bird_tablename; struct vrp *v; @@ -51,7 +51,7 @@ output_bird1v4(FILE *out, struct vrp_tre int output_bird1v6(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks, - struct stats *st) + struct vap_tree *vaps, struct stats *st) { extern const char *bird_tablename; struct vrp *v; @@ -80,7 +80,7 @@ output_bird1v6(FILE *out, struct vrp_tre int output_bird2(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks, - struct stats *st) + struct vap_tree *vaps, struct stats *st) { extern const char *bird_tablename; struct vrp *v; Index: output-csv.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/output-csv.c,v retrieving revision 1.12 diff -u -p -r1.12 output-csv.c --- output-csv.c 4 Nov 2021 11:32:55 -0000 1.12 +++ output-csv.c 13 Aug 2022 00:24:25 -0000 @@ -21,7 +21,7 @@ int output_csv(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks, - struct stats *st) + struct vap_tree *vaps, struct stats *st) { struct vrp *v; Index: output-json.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/output-json.c,v retrieving revision 1.26 diff -u -p -r1.26 output-json.c --- output-json.c 15 May 2022 16:43:34 -0000 1.26 +++ output-json.c 13 Aug 2022 00:24:25 -0000 @@ -46,6 +46,9 @@ outputheader_json(FILE *out, struct stat "\t\t\"roas\": %zu,\n" "\t\t\"failedroas\": %zu,\n" "\t\t\"invalidroas\": %zu,\n" + "\t\t\"aspas\": %zu,\n" + "\t\t\"failedaspas\": %zu,\n" + "\t\t\"invalidaspas\": %zu,\n" "\t\t\"bgpsec_pubkeys\": %zu,\n" "\t\t\"certificates\": %zu,\n" "\t\t\"invalidcertificates\": %zu,\n" @@ -55,6 +58,7 @@ outputheader_json(FILE *out, struct stat hn, tbuf, (long long)st->elapsed_time.tv_sec, (long long)st->user_time.tv_sec, (long long)st->system_time.tv_sec, st->roas, st->roas_fail, st->roas_invalid, + st->aspas, st->aspas_fail, st->aspas_invalid, st->brks, st->certs, st->certs_fail, st->tals, talsz - st->tals) < 0) return -1; @@ -76,6 +80,8 @@ outputheader_json(FILE *out, struct stat "\t\t\"repositories\": %zu,\n" "\t\t\"vrps\": %zu,\n" "\t\t\"uniquevrps\": %zu,\n" + "\t\t\"vaps\": %zu,\n" + "\t\t\"uniquevaps\": %zu,\n" "\t\t\"cachedir_del_files\": %zu,\n" "\t\t\"cachedir_superfluous_files\": %zu,\n" "\t\t\"cachedir_del_dirs\": %zu\n" @@ -85,14 +91,80 @@ outputheader_json(FILE *out, struct stat st->gbrs, st->repos, st->vrps, st->uniqs, + st->vaps, st->vaps_uniqs, st->del_files, st->extra_files, st->del_dirs) < 0) return -1; return 0; } +static int +print_vap(FILE *out, struct vap *v) +{ + size_t i; + + if (fprintf(out, "\t\t\t{ \"custasid\": %u, \"providers\": [", + v->custasid) < 0) + return -1; + for (i = 0; i < v->providersz; i++) { + if (fprintf(out, "%u", v->providers[i]) < 0) + return -1; + if (i + 1 < v->providersz) + if (fprintf(out, ", ") < 0) + return -1; + } + if (fprintf(out, "], \"expires\": %lld }", (long long)v->expires) < 0) + return -1; + + return 0; +} + +static int +output_aspa(FILE *out, struct vap_tree *vaps) +{ + struct vap *v; + int first; + + if (fprintf(out, "\n\t],\n\n\t\"provider_authorizations\": {\n" + "\t\t\"ipv4\": [\n") < 0) + return -1; + + first = 1; + RB_FOREACH(v, vap_tree, vaps) + if (v->afi == AFI_IPV4) { + if (!first) { + if (fprintf(out, ",\n") < 0) + return -1; + } + first = 0; + if (print_vap(out, v)) + return -1; + } + + if (fprintf(out, "\n\t\t],\n\t\t\"ipv6\": [\n") < 0) + return -1; + + first = 1; + RB_FOREACH(v, vap_tree, vaps) + if (v->afi == AFI_IPV6) { + if (!first) { + if (fprintf(out, ",\n") < 0) + return -1; + } + first = 0; + if (print_vap(out, v)) + return -1; + } + + if (fprintf(out, "\n\t\t]\n\t}\n") < 0) + return -1; + + return 0; +} + + int output_json(FILE *out, struct vrp_tree *vrps, struct brk_tree *brks, - struct stats *st) + struct vap_tree *vaps, struct stats *st) { char buf[64]; struct vrp *v; @@ -140,7 +212,11 @@ output_json(FILE *out, struct vrp_tree * return -1; } - if (fprintf(out, "\n\t]\n}\n") < 0) + if (output_aspa(out, vaps) < 0) return -1; + + if (fprintf(out, "\n}\n") < 0) + return -1; + return 0; } Index: output.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/output.c,v retrieving revision 1.26 diff -u -p -r1.26 output.c --- output.c 20 Apr 2022 15:29:24 -0000 1.26 +++ output.c 13 Aug 2022 00:24:25 -0000 @@ -65,7 +65,7 @@ static const struct outputs { int format; char *name; int (*fn)(FILE *, struct vrp_tree *, struct brk_tree *, - struct stats *); + struct vap_tree *, struct stats *); } outputs[] = { { FORMAT_OPENBGPD, "openbgpd", output_bgpd }, { FORMAT_BIRD, "bird1v4", output_bird1v4 }, @@ -83,7 +83,8 @@ static void sig_handler(int); static void set_signal_handler(void); int -outputfiles(struct vrp_tree *v, struct brk_tree *b, struct stats *st) +outputfiles(struct vrp_tree *v, struct brk_tree *b, struct vap_tree *a, + struct stats *st) { int i, rc = 0; @@ -102,7 +103,7 @@ outputfiles(struct vrp_tree *v, struct b rc = 1; continue; } - if ((*outputs[i].fn)(fout, v, b, st) != 0) { + if ((*outputs[i].fn)(fout, v, b, a, st) != 0) { warn("output for %s format failed", outputs[i].name); fclose(fout); output_cleantmp(); Index: parser.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/parser.c,v retrieving revision 1.73 diff -u -p -r1.73 parser.c --- parser.c 21 Apr 2022 12:59:03 -0000 1.73 +++ parser.c 13 Aug 2022 00:24:25 -0000 @@ -492,6 +492,46 @@ proc_parser_gbr(char *file, const unsign } /* + * Parse an ASPA object + */ +static struct aspa * +proc_parser_aspa(char *file, const unsigned char *der, size_t len) +{ + struct aspa *aspa; + struct auth *a; + struct crl *crl; + X509 *x509; + + if ((aspa = aspa_parse(&x509, file, der, len)) == NULL) + return NULL; + + a = valid_ski_aki(file, &auths, aspa->ski, aspa->aki); + crl = crl_get(&crlt, a); + + if (!valid_x509(file, ctx, x509, a, crl, 0)) { + X509_free(x509); + aspa_free(aspa); + return NULL; + } + X509_free(x509); + + aspa->talid = a->cert->talid; + + if (valid_aspa(file, a, aspa)) + aspa->valid = 1; + + if (crl != NULL && aspa->expires > crl->expires) + aspa->expires = crl->expires; + + for (; a != NULL; a = a->parent) { + if (aspa->expires > a->cert->expires) + aspa->expires = a->cert->expires; + } + + return aspa; +} + +/* * Load the file specified by the entity information. */ static char * @@ -522,6 +562,7 @@ parse_entity(struct entityq *q, struct m struct cert *cert; struct mft *mft; struct roa *roa; + struct aspa *aspa; struct ibuf *b; unsigned char *f; size_t flen; @@ -606,6 +647,16 @@ parse_entity(struct entityq *q, struct m file = parse_load_file(entp, &f, &flen); io_str_buffer(b, file); proc_parser_gbr(file, f, flen); + break; + case RTYPE_ASPA: + file = parse_load_file(entp, &f, &flen); + io_str_buffer(b, file); + aspa = proc_parser_aspa(file, f, flen); + c = (aspa != NULL); + io_simple_buffer(b, &c, sizeof(int)); + if (aspa != NULL) + aspa_buffer(b, aspa); + aspa_free(aspa); break; default: errx(1, "unhandled entity type %d", entp->type); Index: print.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/print.c,v retrieving revision 1.14 diff -u -p -r1.14 print.c --- print.c 14 Jul 2022 13:24:56 -0000 1.14 +++ print.c 13 Aug 2022 00:24:25 -0000 @@ -568,3 +568,52 @@ rsc_print(const X509 *x, const struct rs if (outformats & FORMAT_JSON) printf("\t],\n"); } + +void +aspa_print(const X509 *x, const struct aspa *p) +{ + size_t i; + + if (outformats & FORMAT_JSON) { + printf("\t\"type\": \"aspa\",\n"); + printf("\t\"ski\": \"%s\",\n", pretty_key_id(p->ski)); + x509_print(x); + printf("\t\"aki\": \"%s\",\n", pretty_key_id(p->aki)); + printf("\t\"aia\": \"%s\",\n", p->aia); + printf("\t\"customer_asid\": %u,\n", p->custasid); + printf("\t\"provider_set\": [\n"); + for (i = 0; i < p->providersz; i++) { + printf("\t\t{ \"asid\": %u", p->providers[i].as); + if (p->providers[i].afi == AFI_IPV4) + printf(", \"afi_limit\": \"ipv4\""); + if (p->providers[i].afi == AFI_IPV6) + printf(", \"afi_limit\": \"ipv6\""); + printf(" }"); + if (i + 1 < p->providersz) + printf(","); + printf("\n"); + } + printf("\t],\n"); + } else { + printf("Subject key identifier: %s\n", pretty_key_id(p->ski)); + x509_print(x); + printf("Authority key identifier: %s\n", pretty_key_id(p->aki)); + printf("Authority info access: %s\n", p->aia); + printf("Customer AS: %u\n", p->custasid); + printf("Provider Set:\n"); + for (i = 0; i < p->providersz; i++) { + printf("%5zu: AS: %d", i + 1, p->providers[i].as); + switch (p->providers[i].afi) { + case AFI_IPV4: + printf(" (IPv4 only)"); + break; + case AFI_IPV6: + printf(" (IPv6 only)"); + break; + default: + break; + } + printf("\n"); + } + } +} Index: roa.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/roa.c,v retrieving revision 1.49 diff -u -p -r1.49 roa.c --- roa.c 10 Aug 2022 14:54:03 -0000 1.49 +++ roa.c 13 Aug 2022 00:24:25 -0000 @@ -389,6 +389,8 @@ vrpcmp(struct vrp *a, struct vrp *b) if (rv) return rv; break; + default: + break; } /* a smaller prefixlen is considered bigger, e.g. /8 vs /10 */ if (a->addr.prefixlen < b->addr.prefixlen) Index: rpki-client.8 =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/rpki-client.8,v retrieving revision 1.68 diff -u -p -r1.68 rpki-client.8 --- rpki-client.8 30 Jun 2022 10:27:52 -0000 1.68 +++ rpki-client.8 13 Aug 2022 00:24:25 -0000 @@ -295,6 +295,8 @@ Certification Requests. Resource Public Key Infrastructure (RPKI) Trust Anchor Locator. .It draft-ietf-sidrops-rpki-rsc-08 A profile for Resource Public Key Infrastructure (RPKI) Signed Checklists (RSC). +.It draft-ietf-sidrops-aspa-profile-10 +A Profile for Autonomous System Provider Authorization (ASPA). .El .Sh HISTORY .Nm Index: validate.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/validate.c,v retrieving revision 1.40 diff -u -p -r1.40 validate.c --- validate.c 10 Jun 2022 10:36:43 -0000 1.40 +++ validate.c 13 Aug 2022 00:24:25 -0000 @@ -533,3 +533,20 @@ valid_econtent_version(const char *fn, c return 0; } } + +/* + * Validate the ASPA: check that the customerASID is contained. + * Returns 1 if valid, 0 otherwise. + */ +int +valid_aspa(const char *fn, struct auth *a, struct aspa *aspa) +{ + + if (valid_as(a, aspa->custasid, aspa->custasid)) + return 1; + else + warnx("%s: ASPA: uncovered Customer ASID: %u", fn, + aspa->custasid); + + return 0; +} Index: x509.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/x509.c,v retrieving revision 1.47 diff -u -p -r1.47 x509.c --- x509.c 28 Jul 2022 16:03:19 -0000 1.47 +++ x509.c 13 Aug 2022 00:24:25 -0000 @@ -44,6 +44,7 @@ ASN1_OBJECT *msg_dgst_oid; /* pkcs-9 id- ASN1_OBJECT *sign_time_oid; /* pkcs-9 id-signingTime */ ASN1_OBJECT *bin_sign_time_oid; /* pkcs-9 id-aa-binarySigningTime */ ASN1_OBJECT *rsc_oid; /* id-ct-signedChecklist */ +ASN1_OBJECT *aspa_oid; /* id-ct-ASPA */ void x509_init_oid(void) @@ -81,6 +82,9 @@ x509_init_oid(void) if ((rsc_oid = OBJ_txt2obj("1.2.840.113549.1.9.16.1.48", 1)) == NULL) errx(1, "OBJ_txt2obj for %s failed", "1.2.840.113549.1.9.16.1.48"); + if ((aspa_oid = OBJ_txt2obj("1.2.840.113549.1.9.16.1.49", 1)) == NULL) + errx(1, "OBJ_txt2obj for %s failed", + "1.2.840.113549.1.9.16.1.49"); } /*