On Fri, Nov 25, 2022 at 09:36:41AM +0000, Job Snijders wrote: [...] > $ ftp -MV https://sobornost.net/geofeed.csv
Your example file contains 2001:67c:208c::/48,NL,NL-NH,Amsterdam I think this is missing a comma. There is an optional postal_code field and RFC 8805, section 2.1 says: "the requisite minimum number of commas SHOULD be present." Below a first pass. It's gotten a bit lengthy since I think that geofeed_parse() needs to be reworked completely. Other than that the adaptations to my suggestions should be rather straightforward. > Index: Makefile > =================================================================== > RCS file: /cvs/src/usr.sbin/rpki-client/Makefile,v > retrieving revision 1.27 > diff -u -p -r1.27 Makefile > --- Makefile 2 Nov 2022 12:43:02 -0000 1.27 > +++ Makefile 25 Nov 2022 09:26:11 -0000 > @@ -1,11 +1,11 @@ > # $OpenBSD: Makefile,v 1.27 2022/11/02 12:43:02 job Exp $ > > PROG= rpki-client > -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 tak.c tal.c validate.c x509.c > +SRCS= as.c aspa.c cert.c cms.c crl.c encoding.c filemode.c gbr.c > geofeed.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 tak.c tal.c validate.c x509.c > MAN= rpki-client.8 > > LDADD+= -lexpat -ltls -lssl -lcrypto -lutil > Index: cms.c > =================================================================== > RCS file: /cvs/src/usr.sbin/rpki-client/cms.c,v > retrieving revision 1.21 > diff -u -p -r1.21 cms.c > --- cms.c 12 Aug 2022 13:19:02 -0000 1.21 > +++ cms.c 25 Nov 2022 09:26:11 -0000 > @@ -23,6 +23,7 @@ > #include <string.h> > #include <unistd.h> > > +#include <openssl/bio.h> > #include <openssl/cms.h> > > #include "extern.h" > @@ -32,38 +33,32 @@ extern ASN1_OBJECT *msg_dgst_oid; > extern ASN1_OBJECT *sign_time_oid; > extern ASN1_OBJECT *bin_sign_time_oid; > > -/* > - * Parse and validate a self-signed CMS message, where the signing X509 > - * certificate has been hashed to dgst (optional). > - * Conforms to RFC 6488. > - * The eContentType of the message must be an oid object. > - * Return the eContent as a string and set "rsz" to be its length. > - */ > -unsigned char * > -cms_parse_validate(X509 **xp, const char *fn, const unsigned char *der, > - size_t derlen, const ASN1_OBJECT *oid, size_t *rsz) > +static int > +cms_parse_validate_internal(X509 **xp, const char *fn, const unsigned char > *der, > + size_t derlen, const ASN1_OBJECT *oid, BIO *bio, unsigned char **res, > + size_t *rsz) > { > char buf[128], obuf[128]; > const ASN1_OBJECT *obj, *octype; > ASN1_OCTET_STRING **os = NULL, *kid = NULL; > CMS_ContentInfo *cms; > - int rc = 0; > STACK_OF(X509) *certs = NULL; > STACK_OF(X509_CRL) *crls; > STACK_OF(CMS_SignerInfo) *sinfos; > CMS_SignerInfo *si; > X509_ALGOR *pdig, *psig; > - unsigned char *res = NULL; > int i, nattrs, nid; > int has_ct = 0, has_md = 0, has_st = 0, > has_bst = 0; > + int rc = 0; > > - *rsz = 0; > *xp = NULL; > + if (rsz != NULL) > + *rsz = 0; I guess that's fine. Instead of doing this, cms_parse_validate_detached() could also pass in a dummy rsz that it ignores. Not sure if that's better. > > /* just fail for empty buffers, the warning was printed elsewhere */ > if (der == NULL) > - return NULL; > + return 0; > > if ((cms = d2i_CMS_ContentInfo(NULL, &der, derlen)) == NULL) { > cryptowarnx("%s: RFC 6488: failed CMS parse", fn); > @@ -74,16 +69,14 @@ cms_parse_validate(X509 **xp, const char > * The CMS is self-signed with a signing certifiate. > * Verify that the self-signage is correct. > */ > - > - if (!CMS_verify(cms, NULL, NULL, NULL, NULL, > + if (!CMS_verify(cms, NULL, NULL, bio, NULL, > CMS_NO_SIGNER_CERT_VERIFY)) { > - cryptowarnx("%s: RFC 6488: CMS not self-signed", fn); > + cryptowarnx("%s: invalid CMS", fn); > goto out; > } > > /* RFC 6488 section 3 verify the CMS */ > /* the version of SignedData and SignerInfos can't be verified */ > - > sinfos = CMS_get0_SignerInfos(cms); > assert(sinfos != NULL); > if (sk_CMS_SignerInfo_num(sinfos) != 1) { > @@ -173,7 +166,6 @@ cms_parse_validate(X509 **xp, const char > } > > /* RFC 6488 section 2.1.3.1: check the object's eContentType. */ > - > obj = CMS_get0_eContentType(cms); > if (obj == NULL) { > warnx("%s: RFC 6488 section 2.1.3.1: eContentType: " > @@ -189,8 +181,8 @@ cms_parse_validate(X509 **xp, const char > } > > /* Compare content-type with eContentType */ > - octype = CMS_signed_get0_data_by_OBJ(si, cnt_type_oid, > - -3, V_ASN1_OBJECT); > + octype = CMS_signed_get0_data_by_OBJ(si, cnt_type_oid, -3, > + V_ASN1_OBJECT); > assert(octype != NULL); > if (OBJ_cmp(obj, octype) != 0) { > OBJ_obj2txt(buf, sizeof(buf), obj, 1); > @@ -215,7 +207,6 @@ cms_parse_validate(X509 **xp, const char > * signing authority according to RFC 6488, 2.1.4. > * We extract that certificate now for later verification. > */ > - > certs = CMS_get0_signers(cms); > if (certs == NULL || sk_X509_num(certs) != 1) { > warnx("%s: RFC 6488 section 2.1.4: eContent: " > @@ -244,35 +235,71 @@ cms_parse_validate(X509 **xp, const char > goto out; > } > > - /* Verify that we have eContent to disseminate. */ > - > - if ((os = CMS_get0_content(cms)) == NULL || *os == NULL) { > - warnx("%s: RFC 6488 section 2.1.4: " > - "eContent: zero-length content", fn); > - goto out; > - } > - > /* > - * Extract and duplicate the eContent. > - * The CMS framework offers us no other way of easily managing > - * this information; and since we're going to d2i it anyway, > - * simply pass it as the desired underlying types. > + * In the detached sig case: no eContent to extract, so jump to out. > + * else, verify that we have eContent to disseminate. > */ > + if (res == NULL) { > + rc = 1; > + goto out; > + } else { This if/else is a bit messy and the if part doesn't really do anything. If you really want to indent this code, the better solution would be to keep the rc = 1 before the out label and do if (res != NULL) { ... } rc = 1; out: However, I would prefer the skipping approach for now. This makes it easier to review since it becomes obvious what actually changes in the code between here and the end. if (res == NULL) { rc = 1; goto out; } In a subsequent step we can see if factoring this check and the skipped code into a small helper function would make things cleaner. > + if ((os = CMS_get0_content(cms)) == NULL || *os == NULL) { > + warnx("%s: RFC 6488 section 2.1.4: " > + "eContent: zero-length content", fn); > + goto out; > + } > > - if ((res = malloc((*os)->length)) == NULL) > - err(1, NULL); > - memcpy(res, (*os)->data, (*os)->length); > - *rsz = (*os)->length; > - > - rc = 1; > -out: > - sk_X509_free(certs); > - CMS_ContentInfo_free(cms); > + /* > + * Extract and duplicate the eContent. > + * The CMS framework offers us no other way of easily managing > + * this information; and since we're going to d2i it anyway, > + * simply pass it as the desired underlying types. > + */ > + if ((*res = malloc((*os)->length)) == NULL) > + err(1, NULL); > + memcpy(*res, (*os)->data, (*os)->length); > + *rsz = (*os)->length; > > + rc = 1; > + } > + out: > if (rc == 0) { > X509_free(*xp); > *xp = NULL; > } > + sk_X509_free(certs); > + CMS_ContentInfo_free(cms); > + return rc; > +} > + > +/* > + * Parse and validate a self-signed CMS message. > + * Conforms to RFC 6488. > + * The eContentType of the message must be an oid object. > + * Return the eContent as a string and set "rsz" to be its length. > + */ > +unsigned char * > +cms_parse_validate(X509 **xp, const char *fn, const unsigned char *der, > + size_t derlen, const ASN1_OBJECT *oid, size_t *rsz) > +{ > + unsigned char *res = NULL; > + > + if (!cms_parse_validate_internal(xp, fn, der, derlen, oid, NULL, &res, > + rsz)) > + return NULL; > > return res; > +} > + > +/* > + * Parse and validate a detached CMS signature. > + * bio must contain the original message, der must contain the CMS. > + * Return the 1 on success, 0 on failure. > + */ > +int > +cms_parse_validate_detached(X509 **xp, const char *fn, const unsigned char > *der, > + size_t derlen, const ASN1_OBJECT *oid, BIO *bio) > +{ > + return cms_parse_validate_internal(xp, fn, der, derlen, oid, bio, NULL, > + NULL); > } > Index: extern.h > =================================================================== > RCS file: /cvs/src/usr.sbin/rpki-client/extern.h,v > retrieving revision 1.160 > diff -u -p -r1.160 extern.h > --- extern.h 18 Nov 2022 14:38:34 -0000 1.160 > +++ extern.h 25 Nov 2022 09:26:11 -0000 > @@ -175,6 +175,7 @@ enum rtype { > RTYPE_RSC, > RTYPE_ASPA, > RTYPE_TAK, > + RTYPE_GEOFEED, > }; > > enum location { > @@ -297,6 +298,21 @@ struct tak { > }; > > /* > + * A geofeed file > + */ > +struct geofeed { > + struct cert_ip *ips; /* IP prefixes in the CSV */ > + size_t ipsz; /* number of IPs */ > + char *aia; /* AIA */ > + char *aki; /* AKI */ > + char *ski; /* SKI */ > + time_t expires; /* Not After of the Geofeed EE */ > + int valid; /* all resources covered */ > + size_t locsz; > + char **locs; /* location info */ Since ips and locs aren't two independent lists, it would be more appropriate to have struct geoip { struct cert_ip ip; char *loc; }; struct geofeed { struct geoip *geoips; size_t geoipsz; AIA, etc... } > +}; > + > +/* > * A single Ghostbuster record > */ > struct gbr { > @@ -565,6 +581,9 @@ void gbr_free(struct gbr *); > struct gbr *gbr_parse(X509 **, const char *, const unsigned char *, > size_t); > > +void geofeed_free(struct geofeed *); > +struct geofeed *geofeed_parse(X509 **, const char *, char *, size_t); > + > void rsc_free(struct rsc *); > struct rsc *rsc_parse(X509 **, const char *, const unsigned char *, > size_t); > @@ -608,12 +627,18 @@ int valid_x509(char *, X509_STORE_CTX > int valid_rsc(const char *, struct cert *, struct rsc *); > int valid_econtent_version(const char *, const ASN1_INTEGER *); > int valid_aspa(const char *, struct cert *, struct aspa *); > +int valid_geofeed(const char *, struct cert *, struct geofeed *); > > /* Working with CMS. */ > unsigned char *cms_parse_validate(X509 **, const char *, > const unsigned char *, size_t, > const ASN1_OBJECT *, size_t *); > > +/* Working with CMS detached signatures */ I would leave out the comment and simply put the prototype next to cms_parse_validate() > +int cms_parse_validate_detached(X509 **, const char *, > + const unsigned char *, size_t, > + const ASN1_OBJECT *, BIO *); > + > /* Work with RFC 3779 IP addresses, prefixes, ranges. */ > > int ip_addr_afi_parse(const char *, const ASN1_OCTET_STRING *, > @@ -759,6 +784,7 @@ void gbr_print(const X509 *, const str > void rsc_print(const X509 *, const struct rsc *); > void aspa_print(const X509 *, const struct aspa *); > void tak_print(const X509 *, const struct tak *); > +void geofeed_print(const X509 *, const struct geofeed *); > > /* Output! */ > > Index: filemode.c > =================================================================== > RCS file: /cvs/src/usr.sbin/rpki-client/filemode.c,v > retrieving revision 1.16 > diff -u -p -r1.16 filemode.c > --- filemode.c 4 Nov 2022 17:39:36 -0000 1.16 > +++ filemode.c 25 Nov 2022 09:26:11 -0000 > @@ -270,6 +270,7 @@ proc_parser_file(char *file, unsigned ch > struct rsc *rsc = NULL; > struct aspa *aspa = NULL; > struct tak *tak = NULL; > + struct geofeed *geofeed = NULL; > char *aia = NULL, *aki = NULL; > char filehash[SHA256_DIGEST_LENGTH]; > char *hash; > @@ -385,6 +386,14 @@ proc_parser_file(char *file, unsigned ch > aia = tak->aia; > aki = tak->aki; > break; > + case RTYPE_GEOFEED: > + geofeed = geofeed_parse(&x509, file, buf, len); > + if (geofeed == NULL) > + break; > + geofeed_print(x509, geofeed); > + aia = geofeed->aia; > + aki = geofeed->aki; > + break; > default: > printf("%s: unsupported file type\n", file); > break; > @@ -420,6 +429,9 @@ proc_parser_file(char *file, unsigned ch > case RTYPE_ASPA: > status = aspa->valid; > break; > + case RTYPE_GEOFEED: > + status = geofeed->valid; > + break; > default: > break; > } > @@ -479,6 +491,7 @@ proc_parser_file(char *file, unsigned ch > rsc_free(rsc); > aspa_free(aspa); > tak_free(tak); > + geofeed_free(geofeed); > } > > /* > Index: geofeed.c > =================================================================== > RCS file: geofeed.c > diff -N geofeed.c > --- /dev/null 1 Jan 1970 00:00:00 -0000 > +++ geofeed.c 25 Nov 2022 09:26:11 -0000 > @@ -0,0 +1,233 @@ > +/* $OpenBSD: geofeed.c,v 1.18 2022/11/02 12:46:49 job Exp $ */ > +/* > + * Copyright (c) 2022 Job Snijders <j...@fastly.com> > + * Copyright (c) 2019 Kristaps Dzonsons <krist...@bsd.lv> > + * > + * 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 <stdlib.h> > +#include <string.h> > + > +#include <arpa/inet.h> > +#include <sys/socket.h> > +#include <openssl/bio.h> > +#include <openssl/x509.h> > + > +#include "extern.h" > + > +/* > + * Parse results and data of the Signed Checklist file. > + */ > +struct parse { > + const char *fn; > + struct geofeed *res; > +}; > + > +extern ASN1_OBJECT *geofeed_oid; > + > +/* > + * Take a CIDR prefix (in presentation format) and add it to parse results. > + * Returns 1 on success. > + */ > +static int > +geofeed_parse_cidr(struct parse *p, char *cidr) > +{ With the suggestion of using struct geoip in struct geofeed, this should be changed to static int geofeed_parse_geoip(struct parse *p, char *cidr, char *loc) and you'd populate both the ip and the loc members in this function. This gets rid of one recallocarray(). In the caller you'd do if (!geofeed_parse_geoip(&p.res, cidr, delim + 1)) goto err; > + struct cert_ip *res; > + struct ip_addr *ipaddr; > + enum afi afi; > + int plen; > + > + if ((ipaddr = calloc(1, sizeof(struct ip_addr))) == NULL) > + err(1, NULL); > + > + if ((plen = inet_net_pton(AF_INET, cidr, ipaddr->addr, > + sizeof(ipaddr->addr))) != -1) > + afi = AFI_IPV4; > + else if ((plen = inet_net_pton(AF_INET6, cidr, ipaddr->addr, > + sizeof(ipaddr->addr))) != -1) > + afi = AFI_IPV6; > + else { > + warnx("invalid address: %s", cidr); Since we haven't validated the file's signature, we should probably strnvis this similar to http_info(). > + return 0; > + } > + This completely ignores the requirements in RFC 8805 2.1.3. Should we perform duplicate checks, keep only the longest prefix, etc, or do you prefer to dump all the file contains and not care too much about 8805? Another option would be to keep both a minimized list and a full list and dump both. > + ipaddr->prefixlen = plen; > + > + p->res->ips = recallocarray(p->res->ips, p->res->ipsz, > + p->res->ipsz + 1, sizeof(struct cert_ip)); > + if (p->res->ips == NULL) > + err(1, NULL); > + > + res = &p->res->ips[p->res->ipsz++]; > + res->type = CERT_IP_ADDR; > + res->ip = *ipaddr; > + res->afi = afi; > + > + if (!ip_cert_compose_ranges(res)) > + return 0; > + > + return 1; > +} > + > +/* > + * Parse a full RFC 9092 file. > + * Returns the Geofeed, or NULL if the object was malformed. > + */ > +struct geofeed * > +geofeed_parse(X509 **x509, const char *fn, char *buf, size_t len) > +{ > + struct parse p; > + char *delim, *line; > + BIO *bio; > + char *b64; > + unsigned char *der; > + size_t dersz; > + const ASN1_TIME *at; > + struct cert *cert = NULL; > + int rc = 0; > + > + bio = BIO_new(BIO_s_mem()); > + assert(bio != NULL); > + > + memset(&p, 0, sizeof(struct parse)); > + p.fn = fn; > + > + if ((p.res = calloc(1, sizeof(struct geofeed))) == NULL) > + err(1, NULL); > + > + if ((b64 = calloc(1, len)) == NULL) > + err(1, NULL); > + > + while ((line = strsep(&buf, "\n"))) { I would prefer using the same memchr() idiom like in tal.c. That allows us to keep track of the length of buf and the line length. > + > + /* zap optional CR, canonicalization happens later */ The CR at the end of the line is required by RFC 8805. > + delim = memchr(line, '\r', strlen(line)); > + if (delim != NULL) > + *delim = '\0'; This truncates line at the first embedded '\r'. What we want is trim the mandatory '\r' at the end of line. > + > + /* read the Geofeed CSV records */ > + if (*line != '#') { I'd prefer if (line[0] != '#') { However, I think this is not correct. The Geofeed file may have comment lines that aren't part of the signature, and those should be covered by the signature. > + BIO_puts(bio, line); > + BIO_puts(bio, "\r\n"); /* canonicalization */ These two need error checking, at least a <= 0 check. (Memory BIOs resize themselves to accomodate more data, and this could fail): if (BIO_puts(bio, ...) <= 0) goto out; > + > + delim = memchr(line, ',', strlen(line)); As discussed elsewhere, delim needs a NULL check before use. > + *delim = '\0'; > + > + if (!geofeed_parse_cidr(&p, line)) > + goto out; > + > + /* store the geo location */ > + p.res->locs = recallocarray(p.res->locs, p.res->locsz, > + p.res->locsz + 1, sizeof(char *)); > + if (p.res->locs == NULL) > + err(1, NULL); > + > + p.res->locs[p.res->locsz] = strdup(delim + 1); Since you asked elsewhere, this is fine. We know that what follows strdup is a string, so there will be at least one NUL later. There's no need to tokenize the CSV further. It's only printed after the signature is validated, so that's fine. > + if (p.res->locs[p.res->locsz] == NULL) > + goto out; > + > + p.res->locsz++; > + > + } else { /* read the base64 encoded CMS signature */ > + if (strncmp(line, "# RPKI Signature:", > + strlen("# RPKI Signature:")) == 0) > + continue; > + > + if (strncmp(line, "# End Signature:", > + strlen("# End Signature:")) == 0) > + break; > + > + /* skip over '# ' prefix */ > + strlcat(b64, line + 2, len); Where does the spec say '# '? The examples do this. I guess we can assume this for now... > + } I think this is too simple-minded. As mentioned above, the geofeed file can have other comment lines than the ones forming the signature. if (end_signature_seen) { /* * If anything follows the '# End Signature: ...' marker, * that's an error (or at least a warning) since the. */ } if (rpki_signature_seen) { 1. End of signature marker? If so, set end_signature_seen, slurp in next line. 2. Does the line start with "# "? If not, error, otherwise skip 2 chars. 3. If there are more than 72 characters in the line, error. 4. Append to b64. } 5. Is this the start signature marker? If so, set rpki_signature_seen, slurp in next line. (and don't forget a comment that we ignore the address range since it makes no sense) 6. canonicalize (your two BIO_puts) 7. If it's a comment line, slurp in next line. 8. Now do the geofeed_parse_cidr() dance. > + } > + > + if ((base64_decode(b64, strlen(b64), &der, &dersz)) == -1) { > + warnx("base64_decode failed"); > + goto out; > + } > + > + if (!cms_parse_validate_detached(x509, fn, der, dersz, geofeed_oid, > + bio)) > + goto out; > + > + 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: missing AIA, AKI, SIA, or SKI X509 extension", 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)) { > + warnx("%s: ASN1_time_parse failed", fn); > + goto out; > + } > + > + if ((cert = cert_parse_ee_cert(fn, *x509)) == NULL) > + goto out; > + > + if (cert->asz > 0) { > + warnx("%s: superfluous AS Resources extension present", fn); > + goto out; > + } > + > + p.res->valid = valid_geofeed(fn, cert, p.res); > + > + rc = 1; > + out: > + if (rc == 0) { > + geofeed_free(p.res); > + p.res = NULL; > + X509_free(*x509); > + *x509 = NULL; > + } > + cert_free(cert); > + BIO_free(bio); > + > + return p.res; > +} > + > +/* > + * Free what follows a pointer to a geofeed structure. > + * Safe to call with NULL. > + */ > +void > +geofeed_free(struct geofeed *p) > +{ > + size_t i; > + > + if (p == NULL) > + return; > + > + for (i = 0; i < p->locsz; i++) > + free(p->locs[i]); > + free(p->locs); > + free(p->aia); > + free(p->aki); > + free(p->ski); > + free(p->ips); > + free(p); > +} > Index: mft.c > =================================================================== > RCS file: /cvs/src/usr.sbin/rpki-client/mft.c,v > retrieving revision 1.78 > diff -u -p -r1.78 mft.c > --- mft.c 7 Nov 2022 16:23:32 -0000 1.78 > +++ mft.c 25 Nov 2022 09:26:11 -0000 > @@ -170,6 +170,8 @@ rtype_from_file_extension(const char *fn > return RTYPE_ASPA; > if (strcasecmp(fn + sz - 4, ".tak") == 0) > return RTYPE_TAK; > + if (strcasecmp(fn + sz - 4, ".csv") == 0) > + return RTYPE_GEOFEED; > > return RTYPE_INVALID; > } > Index: print.c > =================================================================== > RCS file: /cvs/src/usr.sbin/rpki-client/print.c,v > retrieving revision 1.20 > diff -u -p -r1.20 print.c > --- print.c 16 Nov 2022 08:57:38 -0000 1.20 > +++ print.c 25 Nov 2022 09:26:11 -0000 > @@ -723,3 +723,47 @@ tak_print(const X509 *x, const struct ta > if (outformats & FORMAT_JSON) > printf("\n\t],\n"); > } > + > +void > +geofeed_print(const X509 *x, const struct geofeed *p) > +{ > + char buf[128]; > + size_t i; > + > + if (outformats & FORMAT_JSON) { > + printf("\t\"type\": \"geofeed\",\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\"valid_until\": %lld,\n", (long long)p->expires); > + printf("\t\"records\": [\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("Geofeed valid until: %s\n", time2str(p->expires)); > + printf("Geofeed CSV records:\n"); > + } > + > + for (i = 0; i < p->ipsz; i++) { > + if (p->ips[i].type != CERT_IP_ADDR) > + continue; > + > + ip_addr_print(&p->ips[i].ip, p->ips[i].afi, buf, sizeof(buf)); > + if (outformats & FORMAT_JSON) > + printf("\t\t{ \"prefix\": \"%s\", \"location\": \"%s\"" > + "}", buf, p->locs[i]); > + else > + printf("%5zu: IP: %s (%s)", i + 1, buf, p->locs[i]); > + > + if (outformats & FORMAT_JSON && i + 1 < p->ipsz) > + printf(",\n"); > + else > + printf("\n"); > + } > + > + if (outformats & FORMAT_JSON) > + printf("\t],\n"); > +} > Index: rpki-client.8 > =================================================================== > RCS file: /cvs/src/usr.sbin/rpki-client/rpki-client.8,v > retrieving revision 1.80 > diff -u -p -r1.80 rpki-client.8 > --- rpki-client.8 17 Nov 2022 20:49:38 -0000 1.80 > +++ rpki-client.8 25 Nov 2022 09:26:11 -0000 > @@ -313,6 +313,8 @@ A Profile for BGPsec Router Certificates > Certification Requests. > .It RFC 8630 > Resource Public Key Infrastructure (RPKI) Trust Anchor Locator. > +.It RFC 9092 > +Finding and Using Geofeed Data. > .It RFC 9323 > A Profile for RPKI Signed Checklists (RSCs). > .It draft-ietf-sidrops-aspa-profile-10 > Index: validate.c > =================================================================== > RCS file: /cvs/src/usr.sbin/rpki-client/validate.c,v > retrieving revision 1.46 > diff -u -p -r1.46 validate.c > --- validate.c 2 Nov 2022 11:28:36 -0000 1.46 > +++ validate.c 25 Nov 2022 09:26:11 -0000 > @@ -523,3 +523,28 @@ valid_aspa(const char *fn, struct cert * > > return 0; > } > + > +/* > + * Validate Geofeed prefixes: check that the prefixes are contained. > + * Returns 1 if valid, 0 otherwise. > + */ > +int > +valid_geofeed(const char *fn, struct cert *cert, struct geofeed *geofeed) > +{ > + size_t i; > + char buf[64]; > + > + for (i = 0; i < geofeed->ipsz; i++) { > + if (ip_addr_check_covered(geofeed->ips[i].afi, > + geofeed->ips[i].min, geofeed->ips[i].max, cert->ips, > + cert->ipsz) > 0) > + continue; > + > + ip_addr_print(&geofeed->ips[i].ip, geofeed->ips[i].afi, buf, > + sizeof(buf)); > + warnx("%s: Geofeed: uncovered IP: %s", fn, buf); > + return 0; > + } > + > + return 1; > +} > Index: x509.c > =================================================================== > RCS file: /cvs/src/usr.sbin/rpki-client/x509.c,v > retrieving revision 1.58 > diff -u -p -r1.58 x509.c > --- x509.c 7 Nov 2022 09:18:14 -0000 1.58 > +++ x509.c 25 Nov 2022 09:26:11 -0000 > @@ -47,6 +47,7 @@ ASN1_OBJECT *bin_sign_time_oid; /* pkcs- > ASN1_OBJECT *rsc_oid; /* id-ct-signedChecklist */ > ASN1_OBJECT *aspa_oid; /* id-ct-ASPA */ > ASN1_OBJECT *tak_oid; /* id-ct-SignedTAL */ > +ASN1_OBJECT *geofeed_oid; /* id-ct-geofeedCSVwithCRLF */ > > static const struct { > const char *oid; > @@ -103,6 +104,10 @@ static const struct { > { > .oid = "1.2.840.113549.1.9.16.2.46", > .ptr = &bin_sign_time_oid, > + }, > + { > + .oid = "1.2.840.113549.1.9.16.1.47", > + .ptr = &geofeed_oid, > }, > { > .oid = "1.2.840.113549.1.9.16.1.48", >