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",

Reply via email to