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) {