On Wed, Jan 19, 2022 at 12:56:21PM +0100, Theo Buehler wrote: > On Wed, Jan 19, 2022 at 11:06:06AM +0100, Claudio Jeker wrote: > > The idea is that rpki-client -f file will show a human readable output for > > file. It will also verify that file is valid (or show an error if not). > > > > This implements this as a first version. Especially the output needs > > improvement but parsing and validation works. > > For validation rpki-client needs to follow up the chain of trust by > > traversing the Authority Information Access and the X509v3 CRL > > Distribution Points fields of the certificate and follow them up until a > > TA is found. Once a TA is found the stack can be walked back validating > > each intermediate cert and finally the file in question. While doing that > > all CRLs referenced by any cert are loaded into rpki-client as well. > > Now the TA are loaded at the start by loading the TAL files and then using > > them to validate the TAs. > > > > Since all of this is done in offline mode only the valid/ and ta/ > > directory are used for these lookups. This also simplifies the lookup. > > Just chop of rsync:// from the URI and prepend valid/ to get the path of > > the file. Also do not run the cleanup routines at the end because that > > would remove all of the repo. > > This generally looks good. I have a few nits and questions inline.
> > + if (file != NULL) { > > + size_t sz; > > + > > + sz = strlen(file); > > + if (strcasecmp(file + sz - 4, ".tal") != 0 && > > + strcasecmp(file + sz - 4, ".cer") != 0 && > > + strcasecmp(file + sz - 4, ".crl") != 0 && > > + strcasecmp(file + sz - 4, ".mft") != 0 && > > + strcasecmp(file + sz - 4, ".roa") != 0 && > > + strcasecmp(file + sz - 4, ".gbr") != 0) > > + errx(1, "unsupported or invalid file: %s", file); > > We have so many of these file extension checks now. I wonder if it would > be worth adding a helper function that does this and returns the correct > RTYPE_* (perhaps renaming the unused RTYPE_EOF into RTYPE_INVALID). > This might simplify/streamline a few things. > > Obviously not part of this diff. I could take a stab at this if you're > up for it. It may be useful since there is similar code in the parser. I wanted to exit asap with a unknown file but maybe we want to support multiple files in -f mode and then check makes less sense. I want to prevent things like rpki-client -f /etc/passwd but the check is kind of duplicated here and in parser.c. > > + /* Look for the TA which is hopefully in the auths tree */ > > + if (auth_find(&auths, cert->aki) == NULL) { > > + if (stacksz >= MAX_CERT_DEPTH) { > > + warnx("authority chain exceeds max depth of %i", > > + MAX_CERT_DEPTH); > > + goto done; > > + } > > + stack[stacksz] = cert; > > + filestack[stacksz] = nfile; > > + stacksz++; > > + /* recurse on next certificate */ > > + parse_load_certchain(cert->aia); > > I'm not super keen on recursion in such complicated contexts, but I > don't readily see a way to do this iteratively. Need to ponder this a > bit more. It is a similar issue to X509_verify_cert(). Recursion works nicely here but I guess it is possible to replace it with a for() loop. I would split parse_load_certchain into two, the part that loads the cert and the bits responsible for the chain. > > + return; > > + } > > + > > + /* TA found play back the stack and add all certs */ > > + do { > > + if (parser_cert_validate(nfile, cert) == NULL) > > + break; > > I think this also needs a cert_free(cert). Certs are special and never released once valid. The are added to the auths tree. If parser_cert_validate() fails then cert is already freed so that should also be fine. > > + free(nfile); > > + cert = stack[stacksz - 1]; > > + nfile = filestack[stacksz - 1]; > > I'm probably missing something, but I don't see why we don't hit this > in the last iteration where stacksz == 0 (which would be bad). > > > + } while (stacksz-- > 0); > > It's probably related to my above confusion: We don't need to free cert > if parser_cert_validate() fails but do need to free it if we exit the > while loop because stacksz == 0. Yeah, I'm also confused. I think the code currently underflows the stack which seems to result in a NULL cert. Anyway I totally rewrote that bit without recursion. Have a look below. Only think I dislike is the fact that parse_load_certchain() is using the URI instead of the filename (but should be easy enough to convert between the two). -- :wq Claudio Index: extern.h =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/extern.h,v retrieving revision 1.108 diff -u -p -r1.108 extern.h --- extern.h 18 Jan 2022 16:36:49 -0000 1.108 +++ extern.h 18 Jan 2022 16:52:29 -0000 @@ -294,6 +294,7 @@ enum rtype { RTYPE_CRL, RTYPE_GBR, RTYPE_REPO, + RTYPE_FILE, }; enum http_result { @@ -393,6 +394,7 @@ struct msgbuf; /* global variables */ extern int verbose; +extern int filemode; extern const char *tals[]; extern const char *taldescs[]; extern unsigned int talrepocnt[]; Index: main.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/main.c,v retrieving revision 1.177 diff -u -p -r1.177 main.c --- main.c 19 Jan 2022 09:22:51 -0000 1.177 +++ main.c 19 Jan 2022 11:27:34 -0000 @@ -67,6 +67,7 @@ const char *bird_tablename = "ROAS"; int verbose; int noop; +int filemode; int rrdpon = 1; int repo_timeout; @@ -385,25 +386,23 @@ queue_add_from_mft_set(const struct mft } /* - * Add a local TAL file (RFC 7730) to the queue of files to fetch. + * Add a local file to the queue of files to fetch. */ static void -queue_add_tal(const char *file, int talid) +queue_add_file(const char *file, enum rtype type, int talid) { unsigned char *buf; char *nfile; size_t len; buf = load_file(file, &len); - if (buf == NULL) { - warn("%s", file); - return; - } + if (buf == NULL) + err(1, "%s", file); if ((nfile = strdup(file)) == NULL) err(1, NULL); /* Not in a repository, so directly add to queue. */ - entityq_add(NULL, nfile, RTYPE_TAL, NULL, buf, len, talid); + entityq_add(NULL, nfile, type, NULL, buf, len, talid); } /* @@ -510,11 +509,13 @@ entity_process(struct ibuf *b, struct st io_read_buf(b, &type, sizeof(type)); io_read_str(b, &file); + /* in filemode messages can be ignored, only the accounting matters */ + if (filemode) + goto done; + if (filepath_add(&fpt, file) == 0) { warnx("%s: File already visited", file); - free(file); - entity_queue--; - return; + goto done; } switch (type) { @@ -582,10 +583,13 @@ entity_process(struct ibuf *b, struct st case RTYPE_GBR: st->gbrs++; break; + case RTYPE_FILE: + break; default: errx(1, "unknown entity type %d", type); } +done: free(file); entity_queue--; } @@ -773,6 +777,7 @@ main(int argc, char *argv[]) break; case 'f': file = optarg; + filemode = 1; noop = 1; break; case 'j': @@ -833,20 +838,35 @@ main(int argc, char *argv[]) warnx("cache directory required"); goto usage; } - if (outputdir == NULL) { + if (file != NULL) { + size_t sz; + + sz = strlen(file); + if (strcasecmp(file + sz - 4, ".tal") != 0 && + strcasecmp(file + sz - 4, ".cer") != 0 && + strcasecmp(file + sz - 4, ".crl") != 0 && + strcasecmp(file + sz - 4, ".mft") != 0 && + strcasecmp(file + sz - 4, ".roa") != 0 && + strcasecmp(file + sz - 4, ".gbr") != 0) + errx(1, "unsupported or invalid file: %s", file); + + outputdir = NULL; + } else if (outputdir == NULL) { warnx("output directory required"); goto usage; } if ((cachefd = open(cachedir, O_RDONLY | O_DIRECTORY)) == -1) err(1, "cache directory %s", cachedir); - if ((outdirfd = open(outputdir, O_RDONLY | O_DIRECTORY)) == -1) - err(1, "output directory %s", outputdir); + if (outputdir != NULL) { + if ((outdirfd = open(outputdir, O_RDONLY | O_DIRECTORY)) == -1) + err(1, "output directory %s", outputdir); + if (outformats == 0) + outformats = FORMAT_OPENBGPD; + } check_fs_size(cachefd, cachedir); - if (outformats == 0) - outformats = FORMAT_OPENBGPD; if (talsz == 0) talsz = tal_load_default(); @@ -1049,7 +1069,10 @@ main(int argc, char *argv[]) */ for (i = 0; i < talsz; i++) - queue_add_tal(tals[i], i); + queue_add_file(tals[i], RTYPE_TAL, i); + + if (file != NULL) + queue_add_file(file, RTYPE_FILE, 0); /* change working directory to the cache directory */ if (fchdir(cachefd) == -1) @@ -1211,6 +1234,10 @@ main(int argc, char *argv[]) /* processing did not finish because of error */ if (entity_queue != 0) errx(1, "not all files processed, giving up"); + + /* if processing in filemode the process is done, no cleanup */ + if (filemode) + return rc; logx("all files parsed: generating output"); Index: parser.c =================================================================== RCS file: /cvs/src/usr.sbin/rpki-client/parser.c,v retrieving revision 1.44 diff -u -p -r1.44 parser.c --- parser.c 18 Jan 2022 18:19:47 -0000 1.44 +++ parser.c 19 Jan 2022 13:54:06 -0000 @@ -383,24 +383,13 @@ proc_parser_mft(char *file, const unsign } /* - * Certificates are from manifests (has a digest and is signed with - * another certificate) Parse the certificate, make sure its - * signatures are valid (with CRLs), then validate the RPKI content. - * This returns a certificate (which must not be freed) or NULL on - * parse failure. + * Validate a certificate, if invalid free the resouces and return NULL. */ static struct cert * -proc_parser_cert(char *file, const unsigned char *der, size_t len) +parser_cert_validate(char *file, struct cert *cert) { - struct cert *cert; - struct auth *a; - struct crl *crl; - - /* Extract certificate data and X509. */ - - cert = cert_parse(file, der, len); - if (cert == NULL) - return NULL; + struct auth *a; + struct crl *crl; a = valid_ski_aki(file, &auths, cert->ski, cert->aki); crl = get_crl(a); @@ -428,6 +417,28 @@ proc_parser_cert(char *file, const unsig } /* + * Certificates are from manifests (has a digest and is signed with + * another certificate) Parse the certificate, make sure its + * signatures are valid (with CRLs), then validate the RPKI content. + * This returns a certificate (which must not be freed) or NULL on + * parse failure. + */ +static struct cert * +proc_parser_cert(char *file, const unsigned char *der, size_t len) +{ + struct cert *cert; + + /* Extract certificate data. */ + + cert = cert_parse(file, der, len); + if (cert == NULL) + return NULL; + + cert = parser_cert_validate(file, cert); + return cert; +} + +/* * Root certificates come from TALs (has a pkey and is self-signed). * Parse the certificate, ensure that it's public key matches the * known public key from the TAL, and then validate the RPKI @@ -446,7 +457,7 @@ proc_parser_root_cert(char *file, const struct cert *cert; X509 *x509; - /* Extract certificate data and X509. */ + /* Extract certificate data. */ cert = ta_parse(file, der, len, pkey, pkeysz); if (cert == NULL) @@ -669,6 +680,9 @@ fail: return file; } +/* + * Process an entity and responing to parent process. + */ static void parse_entity(struct entityq *q, struct msgbuf *msgq) { @@ -762,6 +776,317 @@ parse_entity(struct entityq *q, struct m } /* + * Use the X509 CRL Distribution Points to locate the CRL needed for + * verification. + */ +static void +parse_load_crl(const char *uri) +{ + char *nfile, *f; + size_t flen; + + if (uri == NULL) + return; + if (strncmp(uri, "rsync://", strlen("rsync://")) != 0) { + warnx("bad CRL distribution point URI %s", uri); + return; + } + uri += strlen("rsync://"); + + if (asprintf(&nfile, "valid/%s", uri) == -1) + err(1, NULL); + + f = load_file(nfile, &flen); + if (f == NULL) { + warn("parse file %s", nfile); + goto done; + } + + proc_parser_crl(nfile, f, flen); + +done: + free(nfile); + free(f); +} + +/* + * Parse the cert pointed at by the AIA URI while doing that also load + * the CRL of this cert. While the CRL is validated the returned cert + * is not. The caller needs to make sure it is validated once all + * necessary certs were loaded. Returns NULL on failure. + */ +static struct cert * +parse_load_cert(const char *uri) +{ + struct cert *cert = NULL; + char *nfile, *f; + size_t flen; + + if (uri == NULL) + return NULL; + + if (strncmp(uri, "rsync://", strlen("rsync://")) != 0) { + warnx("bad authority information access URI %s", uri); + return NULL; + } + uri += strlen("rsync://"); + + if (asprintf(&nfile, "valid/%s", uri) == -1) + err(1, NULL); + + f = load_file(nfile, &flen); + if (f == NULL) { + warn("parse file %s", nfile); + goto done; + } + + cert = cert_parse(nfile, f, flen); + free(f); + + if (cert == NULL) + goto done; + if (cert->purpose != CERT_PURPOSE_CA) { + warnx("AIA reference to bgpsec cert %s", nfile); + goto done; + } + /* try to load the CRL of this cert */ + parse_load_crl(cert->crl); + + free(nfile); + return cert; + +done: + cert_free(cert); + free(nfile); + return NULL; +} + +/* + * Build the certificate chain by using the Authority Information Access. + * This requires that the TA are already validated and added to the auths + * tree. Once the TA is located in the chain the chain is validated in + * reverse order. + */ +static void +parse_load_certchain(char *uri) +{ + struct cert *stack[MAX_CERT_DEPTH]; + char *filestack[MAX_CERT_DEPTH]; + struct cert *cert; + int i, failed; + + for (i = 0; i < MAX_CERT_DEPTH; i++) { + cert = parse_load_cert(uri); + if (cert == NULL) { + warnx("failed to build authority chain"); + return; + } + stack[i] = cert; + filestack[i] = uri; + if (auth_find(&auths, cert->aki) != NULL) { + break; /* found the TA */ + } + uri = cert->aia; + } + + if (i >= MAX_CERT_DEPTH) { + warnx("authority chain exceeds max depth of %i", + MAX_CERT_DEPTH); + for (i = 0; i < MAX_CERT_DEPTH; i++) { + cert_free(stack[i]); + } + return; + } + + /* TA found play back the stack and add all certs */ + for (failed = 0; i >= 0; i--) { + cert = stack[i]; + uri = filestack[i]; + + if (failed) + cert_free(cert); + else if (parser_cert_validate(uri, cert) == NULL) + failed = 1; + } +} + +static void +parse_load_ta(struct tal *tal) +{ + const char *file; + char *nfile, *f; + size_t flen; + + /* does not matter which URI, all end with same filename */ + file = strrchr(tal->uri[0], '/'); + assert(file); + + if (asprintf(&nfile, "ta/%s%s", tal->descr, file) == -1) + err(1, NULL); + + f = load_file(nfile, &flen); + if (f == NULL) { + warn("parse file %s", nfile); + free(nfile); + return; + } + + /* if TA is valid it was added as a root which is all we need */ + proc_parser_root_cert(nfile, f, flen, tal->pkey, tal->pkeysz, tal->id); + free(nfile); + free(f); +} + +/* + * Parse file passed with -f option. + */ +static void +proc_parser_file(char *file, unsigned char *buf, size_t len) +{ + X509 *x509 = NULL; + struct cert *cert = NULL; + struct mft *mft = NULL; + struct roa *roa = NULL; + struct gbr *gbr = NULL; + struct tal *tal = NULL; + enum rtype type; + char *aia = NULL, *aki = NULL, *ski = NULL; + size_t sz; + unsigned long verify_flags = X509_V_FLAG_CRL_CHECK; + + sz = strlen(file); + if (strcasecmp(file + sz - 4, ".tal") == 0) + type = RTYPE_TAL; + else if (strcasecmp(file + sz - 4, ".cer") == 0) + type = RTYPE_CER; + else if (strcasecmp(file + sz - 4, ".crl") == 0) + type = RTYPE_CRL; + else if (strcasecmp(file + sz - 4, ".mft") == 0) + type = RTYPE_MFT; + else if (strcasecmp(file + sz - 4, ".roa") == 0) + type = RTYPE_ROA; + else if (strcasecmp(file + sz - 4, ".gbr") == 0) + type = RTYPE_GBR; + else + errx(1, "%s: unsupported file type", file); + + switch (type) { + case RTYPE_CER: + cert = cert_parse(file, buf, len); + if (cert == NULL) + break; + cert_print(cert); + aia = cert->aia; + aki = cert->aki; + ski = cert->ski; + x509 = cert->x509; + if (X509_up_ref(x509) == 0) + errx(1, "%s: X509_up_ref failed", __func__); + break; + case RTYPE_MFT: + mft = mft_parse(&x509, file, buf, len); + if (mft == NULL) + break; + mft_print(mft); + aia = mft->aia; + aki = mft->aki; + ski = mft->ski; + verify_flags = 0; + break; + case RTYPE_ROA: + roa = roa_parse(&x509, file, buf, len); + if (roa == NULL) + break; + roa_print(roa); + aia = roa->aia; + aki = roa->aki; + ski = roa->ski; + break; + case RTYPE_GBR: + gbr = gbr_parse(&x509, file, buf, len); + if (gbr == NULL) + break; + gbr_print(gbr); + aia = gbr->aia; + aki = gbr->aki; + ski = gbr->ski; + break; + case RTYPE_TAL: + tal = tal_parse(file, buf, len); + if (tal == NULL) + break; + tal_print(tal); + break; + case RTYPE_CRL: /* XXX no printer yet */ + default: + break; + } + + if (aia != NULL) { + struct auth *a; + struct crl *crl; + char *c; + + c = x509_get_crl(x509, file); + parse_load_crl(c); + free(c); + parse_load_certchain(aia); + a = valid_ski_aki(file, &auths, ski, aki); + crl = get_crl(a); + + if (valid_x509(file, x509, a, crl, verify_flags)) + printf("Validation: OK\n"); + else + printf("Validation: Failed\n"); + } + + X509_free(x509); + cert_free(cert); + mft_free(mft); + roa_free(roa); + gbr_free(gbr); + tal_free(tal); +} + +/* + * Process a file request, in general don't send anything back. + */ +static void +parse_file(struct entityq *q, struct msgbuf *msgq) +{ + struct entity *entp; + struct ibuf *b; + struct tal *tal; + + while ((entp = TAILQ_FIRST(q)) != NULL) { + TAILQ_REMOVE(q, entp, entries); + + switch (entp->type) { + case RTYPE_FILE: + proc_parser_file(entp->file, entp->data, entp->datasz); + break; + case RTYPE_TAL: + if ((tal = tal_parse(entp->file, entp->data, + entp->datasz)) == NULL) + errx(1, "%s: could not parse tal file", + entp->file); + tal->id = entp->talid; + parse_load_ta(tal); + tal_free(tal); + break; + default: + errx(1, "unhandled entity type %d", entp->type); + } + + b = io_new_buffer(); + io_simple_buffer(b, &entp->type, sizeof(entp->type)); + io_str_buffer(b, entp->file); + io_close_buffer(msgq, b); + entity_free(entp); + } +} + +/* * Process responsible for parsing and validating content. * All this process does is wait to be told about a file to parse, then * it parses it and makes sure that the data being returned is fully @@ -828,7 +1153,10 @@ proc_parser(int fd) } } - parse_entity(&q, &msgq); + if (!filemode) + parse_entity(&q, &msgq); + else + parse_file(&q, &msgq); } while ((entp = TAILQ_FIRST(&q)) != NULL) {