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

Reply via email to