Hi Theo, Thank you for taking the time to review this.
On Fri, Nov 25, 2022 at 03:18:30PM +0100, Theo Buehler wrote: > 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." Fixed; I uploaded a new signed geofeed.csv file to the same place. > 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. Thanks! Replies inline. > 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; > } Good catch, I've incorporated this. > 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... > } done > > +/* Working with CMS detached signatures */ > > I would leave out the comment and simply put the prototype next to > cms_parse_validate() done > > +/* > > + * 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; done. By passing &p.res; the function signature becomes geofeed_parse_geoip(struct geofeed *res, ...) > > + else { > > + warnx("invalid address: %s", cidr); > > Since we haven't validated the file's signature, we should probably > strnvis this similar to http_info(). done > > + 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. My goal was to focus on verifying the signature; less so on additionally being a RFC 8805 parser. > > +struct geofeed * > > +geofeed_parse(X509 **x509, const char *fn, char *buf, size_t len) > > +{ [snip] > > + > > + 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. OK > > + /* 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. My thinking was to 'fixup' files that somehow went through a 'dos2unix' conversion process. Perhaps it is better to leave that out and not attempt to 'fixup'. > > + > > + /* read the Geofeed CSV records */ > > + if (*line != '#') { > > I'd prefer > > if (line[0] != '#') { OK > 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. I'll add that. > > + 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; done. > > + > > + delim = memchr(line, ',', strlen(line)); > > As discussed elsewhere, delim needs a NULL check before use. done > > + /* 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. OK > } > > 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. OK How about the below? * zaps trailing comments * use() stravis to store the location for prettyprinting * enforces line length limits in the Signature section * uses the tal.c style of iterating through the file Kind regards, Job 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 16:49:56 -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 16:49:56 -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; /* 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,10 +69,9 @@ 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; } @@ -244,7 +238,14 @@ cms_parse_validate(X509 **xp, const char goto out; } - /* Verify that we have eContent to disseminate. */ + /* + * In the detached sig case: there won't be eContent to extract, so + * jump to out. + */ + if (res == NULL) { + rc = 1; + goto out; + } if ((os = CMS_get0_content(cms)) == NULL || *os == NULL) { warnx("%s: RFC 6488 section 2.1.4: " @@ -258,21 +259,50 @@ cms_parse_validate(X509 **xp, const char * 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) + if ((*res = malloc((*os)->length)) == NULL) err(1, NULL); - memcpy(res, (*os)->data, (*os)->length); + memcpy(*res, (*os)->data, (*os)->length); *rsz = (*os)->length; rc = 1; -out: - sk_X509_free(certs); - CMS_ContentInfo_free(cms); - + 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 16:49:57 -0000 @@ -175,6 +175,7 @@ enum rtype { RTYPE_RSC, RTYPE_ASPA, RTYPE_TAK, + RTYPE_GEOFEED, }; enum location { @@ -297,6 +298,27 @@ struct tak { }; /* + * A single geofeed record + */ +struct geoip { + struct cert_ip *ip; + char *loc; +}; + +/* + * A geofeed file + */ +struct geofeed { + struct geoip *geoips; /* Prefix + location entry in the CSV */ + size_t geoipsz; /* 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 */ +}; + +/* * A single Ghostbuster record */ struct gbr { @@ -565,6 +587,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,11 +633,15 @@ 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 *); +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. */ @@ -759,6 +788,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 16:49:57 -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 16:49:57 -0000 @@ -0,0 +1,278 @@ +/* $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 <vis.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_geoip(struct geofeed *res, char *cidr, char *loc) +{ + struct geoip *geoip; + 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 { + static char buf[80]; + if (strnvis(buf, cidr, sizeof(buf), VIS_SAFE) + >= (int)sizeof(buf)) { + memcpy(buf + sizeof(buf) - 4, "...", 4); + } + warnx("invalid address: %s", buf); + return 0; + } + + ipaddr->prefixlen = plen; + + res->geoips = recallocarray(res->geoips, res->geoipsz, + res->geoipsz + 1, sizeof(struct geoip)); + if (res->geoips == NULL) + err(1, NULL); + geoip = &res->geoips[res->geoipsz++]; + + if ((geoip->ip = calloc(1, sizeof(struct cert_ip))) == NULL) + err(1, NULL); + + geoip->ip->type = CERT_IP_ADDR; + geoip->ip->ip = *ipaddr; + geoip->ip->afi = afi; + + if (stravis(&geoip->loc, loc, VIS_SAFE) == -1) + err(1, "stravis"); + + if (!ip_cert_compose_ranges(geoip->ip)) + 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, *nl; + BIO *bio; + char *b64; + size_t b64sz; + unsigned char *der; + size_t dersz; + const ASN1_TIME *at; + struct cert *cert = NULL; + int rpki_signature_seen = 0, end_signature_seen = 0; + 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); + b64sz = len; + + while ((nl = memchr(buf, '\n', len)) != NULL) { + line = buf; + + /* advance buffer to next line */ + len -= nl + 1 - buf; + buf = nl + 1; + + /* replace LF and CR with NUL, point nl at first NUL */ + *nl = '\0'; + if (nl > line && nl[-1] == '\r') { + nl[-1] = '\0'; + nl--; + } else { + warnx("%s: malformed file, expected CRLF line" + " endings", fn); + goto out; + } + + if (end_signature_seen) { + warnx("%s: trailing data after signature section", fn); + goto out; + } + + if (strncmp(line, "# End Signature:", + strlen("# End Signature:")) == 0) { + end_signature_seen = 1; + continue; + } + + if (rpki_signature_seen) { + if (strlen(line) > 72) { + warnx("%s: overly long line in signature " + "section", fn); + goto out; + } + if (line[0] != '#' || line[1] != ' ') { + warnx("%s: missing '# ' prefix in signature " + "section", fn); + goto out; + } + + /* skip over '# ' */ + line += 2; + strlcat(b64, line, b64sz); + continue; + } + + if (strncmp(line, "# RPKI Signature:", + strlen("# RPKI Signature:")) == 0) { + rpki_signature_seen = 1; + continue; + } + + /* + * Read the Geofeed CSV records into a BIO to later on + * calculate the message digest and compare with the one + * in the detached CMS signature. + */ + if (BIO_puts(bio, line) <= 0) + goto out; + if (BIO_puts(bio, "\r\n") <= 0) + goto out; + + /* zap comments */ + delim = memchr(line, '#', strlen(line)); + if (delim != NULL) + *delim = '\0'; + + /* Split prefix and location info */ + delim = memchr(line, ',', strlen(line)); + if (delim == NULL) + goto out; + *delim = '\0'; + + /* read each prefix */ + if (!geofeed_parse_geoip(p.res, line, delim + 1)) + goto out; + } + + 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) +{ + if (p == NULL) + return; + + free(p->geoips); + free(p->aia); + free(p->aki); + free(p->ski); + 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 16:49:57 -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 16:49:57 -0000 @@ -723,3 +723,49 @@ 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->geoipsz; i++) { + if (p->geoips[i].ip->type != CERT_IP_ADDR) + continue; + + ip_addr_print(&p->geoips[i].ip->ip, p->geoips[i].ip->afi, buf, + sizeof(buf)); + if (outformats & FORMAT_JSON) + printf("\t\t{ \"prefix\": \"%s\", \"location\": \"%s\"" + "}", buf, p->geoips[i].loc); + else + printf("%5zu: IP: %s (%s)", i + 1, buf, + p->geoips[i].loc); + + if (outformats & FORMAT_JSON && i + 1 < p->geoipsz) + 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 16:49:57 -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 16:49:57 -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 *g) +{ + size_t i; + char buf[64]; + + for (i = 0; i < g->geoipsz; i++) { + if (ip_addr_check_covered(g->geoips[i].ip->afi, + g->geoips[i].ip->min, g->geoips[i].ip->max, cert->ips, + cert->ipsz) > 0) + continue; + + ip_addr_print(&g->geoips[i].ip->ip, g->geoips[i].ip->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 16:49:57 -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",