/* $OpenBSD: mft.c,v 1.117 2024/06/11 10:38:40 tb Exp $ */ /* * Copyright (c) 2022 Theo Buehler * Copyright (c) 2019 Kristaps Dzonsons * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include "extern.h" extern ASN1_OBJECT *mft_oid; /* * Types and templates for the Manifest eContent, RFC 6486, section 4.2. */ ASN1_ITEM_EXP FileAndHash_it; ASN1_ITEM_EXP Manifest_it; typedef struct { ASN1_IA5STRING *file; ASN1_BIT_STRING *hash; } FileAndHash; DECLARE_STACK_OF(FileAndHash); #ifndef DEFINE_STACK_OF #define sk_FileAndHash_dup(sk) SKM_sk_dup(FileAndHash, (sk)) #define sk_FileAndHash_free(sk) SKM_sk_free(FileAndHash, (sk)) #define sk_FileAndHash_num(sk) SKM_sk_num(FileAndHash, (sk)) #define sk_FileAndHash_value(sk, i) SKM_sk_value(FileAndHash, (sk), (i)) #define sk_FileAndHash_sort(sk) SKM_sk_sort(FileAndHash, (sk)) #define sk_FileAndHash_set_cmp_func(sk, cmp) \ SKM_sk_set_cmp_func(FileAndHash, (sk), (cmp)) #endif typedef struct { ASN1_INTEGER *version; ASN1_INTEGER *manifestNumber; ASN1_GENERALIZEDTIME *thisUpdate; ASN1_GENERALIZEDTIME *nextUpdate; ASN1_OBJECT *fileHashAlg; STACK_OF(FileAndHash) *fileList; } Manifest; ASN1_SEQUENCE(FileAndHash) = { ASN1_SIMPLE(FileAndHash, file, ASN1_IA5STRING), ASN1_SIMPLE(FileAndHash, hash, ASN1_BIT_STRING), } ASN1_SEQUENCE_END(FileAndHash); ASN1_SEQUENCE(Manifest) = { ASN1_EXP_OPT(Manifest, version, ASN1_INTEGER, 0), ASN1_SIMPLE(Manifest, manifestNumber, ASN1_INTEGER), ASN1_SIMPLE(Manifest, thisUpdate, ASN1_GENERALIZEDTIME), ASN1_SIMPLE(Manifest, nextUpdate, ASN1_GENERALIZEDTIME), ASN1_SIMPLE(Manifest, fileHashAlg, ASN1_OBJECT), ASN1_SEQUENCE_OF(Manifest, fileList, FileAndHash), } ASN1_SEQUENCE_END(Manifest); DECLARE_ASN1_FUNCTIONS(Manifest); IMPLEMENT_ASN1_FUNCTIONS(Manifest); #define GENTIME_LENGTH 15 /* * Determine rtype corresponding to file extension. Returns RTYPE_INVALID * on error or unkown extension. */ enum rtype rtype_from_file_extension(const char *fn) { size_t sz; sz = strlen(fn); if (sz < 5) return RTYPE_INVALID; if (strcasecmp(fn + sz - 4, ".tal") == 0) return RTYPE_TAL; if (strcasecmp(fn + sz - 4, ".cer") == 0) return RTYPE_CER; if (strcasecmp(fn + sz - 4, ".crl") == 0) return RTYPE_CRL; if (strcasecmp(fn + sz - 4, ".mft") == 0) return RTYPE_MFT; if (strcasecmp(fn + sz - 4, ".roa") == 0) return RTYPE_ROA; if (strcasecmp(fn + sz - 4, ".gbr") == 0) return RTYPE_GBR; if (strcasecmp(fn + sz - 4, ".sig") == 0) return RTYPE_RSC; if (strcasecmp(fn + sz - 4, ".asa") == 0) return RTYPE_ASPA; if (strcasecmp(fn + sz - 4, ".tak") == 0) return RTYPE_TAK; if (strcasecmp(fn + sz - 4, ".csv") == 0) return RTYPE_GEOFEED; if (strcasecmp(fn + sz - 4, ".spl") == 0) return RTYPE_SPL; return RTYPE_INVALID; } /* * Validate that a filename listed on a Manifest only contains characters * permitted in RFC 9286 section 4.2.2. * Also ensure that there is exactly one '.'. */ static int valid_mft_filename(const char *fn, size_t len) { const unsigned char *c; if (!valid_filename(fn, len)) return 0; c = memchr(fn, '.', len); if (c == NULL || c != memrchr(fn, '.', len)) return 0; return 1; } /* * Check that the file is allowed to be part of a manifest and the parser * for this type is implemented in rpki-client. * Returns corresponding rtype or RTYPE_INVALID to mark the file as unknown. */ static enum rtype rtype_from_mftfile(const char *fn) { enum rtype type; type = rtype_from_file_extension(fn); switch (type) { case RTYPE_CER: case RTYPE_CRL: case RTYPE_GBR: case RTYPE_ROA: case RTYPE_ASPA: case RTYPE_SPL: case RTYPE_TAK: return type; default: return RTYPE_INVALID; } } /* * Parse an individual "FileAndHash", RFC 6486, sec. 4.2. * Return zero on failure, non-zero on success. */ static int mft_parse_filehash(const char *fn, struct mft *mft, const FileAndHash *fh, int *found_crl) { char *file = NULL; int rc = 0; struct mftfile *fent; enum rtype type; size_t new_idx = 0; if (!valid_mft_filename(fh->file->data, fh->file->length)) { warnx("%s: RFC 6486 section 4.2.2: bad filename", fn); goto out; } file = strndup(fh->file->data, fh->file->length); if (file == NULL) err(1, NULL); if (fh->hash->length != SHA256_DIGEST_LENGTH) { warnx("%s: RFC 6486 section 4.2.1: hash: " "invalid SHA256 length, have %d", fn, fh->hash->length); goto out; } type = rtype_from_mftfile(file); if (type == RTYPE_CRL) { if (*found_crl == 1) { warnx("%s: RFC 6487: too many CRLs listed on MFT", fn); goto out; } if (strcmp(file, mft->crl) != 0) { warnx("%s: RFC 6487: name (%s) doesn't match CRLDP " "(%s)", fn, file, mft->crl); goto out; } /* remember the filehash for the CRL in struct mft */ memcpy(mft->crlhash, fh->hash->data, SHA256_DIGEST_LENGTH); *found_crl = 1; } if (filemode) fent = &mft->files[mft->filesz++]; else { /* Fisher-Yates shuffle */ new_idx = arc4random_uniform(mft->filesz + 1); mft->files[mft->filesz++] = mft->files[new_idx]; fent = &mft->files[new_idx]; } fent->type = type; fent->file = file; file = NULL; memcpy(fent->hash, fh->hash->data, SHA256_DIGEST_LENGTH); rc = 1; out: free(file); return rc; } static int mft_fh_cmp_name(const FileAndHash *const *a, const FileAndHash *const *b) { if ((*a)->file->length < (*b)->file->length) return -1; if ((*a)->file->length > (*b)->file->length) return 1; return memcmp((*a)->file->data, (*b)->file->data, (*b)->file->length); } static int mft_fh_cmp_hash(const FileAndHash *const *a, const FileAndHash *const *b) { assert((*a)->hash->length == SHA256_DIGEST_LENGTH); assert((*b)->hash->length == SHA256_DIGEST_LENGTH); return memcmp((*a)->hash->data, (*b)->hash->data, (*b)->hash->length); } /* * Assuming that the hash lengths are validated, this checks that all file names * and hashes in a manifest are unique. Returns 1 on success, 0 on failure. */ static int mft_has_unique_names_and_hashes(const char *fn, const Manifest *mft) { STACK_OF(FileAndHash) *fhs; int i, ret = 0; if ((fhs = sk_FileAndHash_dup(mft->fileList)) == NULL) err(1, NULL); (void)sk_FileAndHash_set_cmp_func(fhs, mft_fh_cmp_name); sk_FileAndHash_sort(fhs); for (i = 0; i < sk_FileAndHash_num(fhs) - 1; i++) { const FileAndHash *curr = sk_FileAndHash_value(fhs, i); const FileAndHash *next = sk_FileAndHash_value(fhs, i + 1); if (mft_fh_cmp_name(&curr, &next) == 0) { warnx("%s: duplicate name: %.*s", fn, curr->file->length, curr->file->data); goto err; } } (void)sk_FileAndHash_set_cmp_func(fhs, mft_fh_cmp_hash); sk_FileAndHash_sort(fhs); for (i = 0; i < sk_FileAndHash_num(fhs) - 1; i++) { const FileAndHash *curr = sk_FileAndHash_value(fhs, i); const FileAndHash *next = sk_FileAndHash_value(fhs, i + 1); if (mft_fh_cmp_hash(&curr, &next) == 0) { warnx("%s: duplicate hash for %.*s and %.*s", fn, curr->file->length, curr->file->data, next->file->length, next->file->data); goto err; } } ret = 1; err: sk_FileAndHash_free(fhs); return ret; } /* * Handle the eContent of the manifest object, RFC 6486 sec. 4.2. * Returns 0 on failure and 1 on success. */ static int mft_parse_econtent(const char *fn, struct mft *mft, const unsigned char *d, size_t dsz) { const unsigned char *oder; Manifest *mft_asn1; FileAndHash *fh; int found_crl, i, rc = 0; oder = d; if ((mft_asn1 = d2i_Manifest(NULL, &d, dsz)) == NULL) { warnx("%s: RFC 6486 section 4: failed to parse Manifest", fn); goto out; } if (d != oder + dsz) { warnx("%s: %td bytes trailing garbage in eContent", fn, oder + dsz - d); goto out; } if (!valid_econtent_version(fn, mft_asn1->version, 0)) goto out; mft->seqnum = x509_convert_seqnum(fn, mft_asn1->manifestNumber); if (mft->seqnum == NULL) goto out; /* * OpenSSL's DER decoder implementation will accept a GeneralizedTime * which doesn't conform to RFC 5280. So, double check. */ if (ASN1_STRING_length(mft_asn1->thisUpdate) != GENTIME_LENGTH) { warnx("%s: embedded from time format invalid", fn); goto out; } if (ASN1_STRING_length(mft_asn1->nextUpdate) != GENTIME_LENGTH) { warnx("%s: embedded until time format invalid", fn); goto out; } if (!x509_get_time(mft_asn1->thisUpdate, &mft->thisupdate)) { warn("%s: parsing manifest thisUpdate failed", fn); goto out; } if (!x509_get_time(mft_asn1->nextUpdate, &mft->nextupdate)) { warn("%s: parsing manifest nextUpdate failed", fn); goto out; } if (mft->thisupdate > mft->nextupdate) { warnx("%s: bad update interval", fn); goto out; } if (OBJ_obj2nid(mft_asn1->fileHashAlg) != NID_sha256) { warnx("%s: RFC 6486 section 4.2.1: fileHashAlg: " "want SHA256 object, have %s", fn, nid2str(OBJ_obj2nid(mft_asn1->fileHashAlg))); goto out; } if (sk_FileAndHash_num(mft_asn1->fileList) >= MAX_MANIFEST_ENTRIES) { warnx("%s: %d exceeds manifest entry limit (%d)", fn, sk_FileAndHash_num(mft_asn1->fileList), MAX_MANIFEST_ENTRIES); goto out; } mft->files = calloc(sk_FileAndHash_num(mft_asn1->fileList), sizeof(struct mftfile)); if (mft->files == NULL) err(1, NULL); found_crl = 0; for (i = 0; i < sk_FileAndHash_num(mft_asn1->fileList); i++) { fh = sk_FileAndHash_value(mft_asn1->fileList, i); if (!mft_parse_filehash(fn, mft, fh, &found_crl)) goto out; } if (!found_crl) { warnx("%s: CRL not part of MFT fileList", fn); goto out; } if (!mft_has_unique_names_and_hashes(fn, mft_asn1)) goto out; rc = 1; out: Manifest_free(mft_asn1); return rc; } /* * Parse the objects that have been published in the manifest. * Return mft if it conforms to RFC 6486, otherwise NULL. */ struct mft * mft_parse(X509 **x509, const char *fn, int talid, const unsigned char *der, size_t len) { struct mft *mft; struct cert *cert = NULL; int rc = 0; size_t cmsz; unsigned char *cms; char *crldp = NULL, *crlfile; time_t signtime = 0; cms = cms_parse_validate(x509, fn, der, len, mft_oid, &cmsz, &signtime); if (cms == NULL) return NULL; assert(*x509 != NULL); if ((mft = calloc(1, sizeof(*mft))) == NULL) err(1, NULL); mft->signtime = signtime; if (!x509_get_aia(*x509, fn, &mft->aia)) goto out; if (!x509_get_aki(*x509, fn, &mft->aki)) goto out; if (!x509_get_sia(*x509, fn, &mft->sia)) goto out; if (!x509_get_ski(*x509, fn, &mft->ski)) goto out; if (mft->aia == NULL || mft->aki == NULL || mft->sia == NULL || mft->ski == NULL) { warnx("%s: RFC 6487 section 4.8: " "missing AIA, AKI, SIA, or SKI X509 extension", fn); goto out; } if (!x509_inherits(*x509)) { warnx("%s: RFC 3779 extension not set to inherit", fn); goto out; } /* get CRL info for later */ if (!x509_get_crl(*x509, fn, &crldp)) goto out; if (crldp == NULL) { warnx("%s: RFC 6487 section 4.8.6: CRL: " "missing CRL distribution point extension", fn); goto out; } crlfile = strrchr(crldp, '/'); if (crlfile == NULL) { warnx("%s: RFC 6487 section 4.8.6: " "invalid CRL distribution point", fn); goto out; } crlfile++; if (!valid_mft_filename(crlfile, strlen(crlfile)) || rtype_from_file_extension(crlfile) != RTYPE_CRL) { warnx("%s: RFC 6487 section 4.8.6: CRL: " "bad CRL distribution point extension", fn); goto out; } if ((mft->crl = strdup(crlfile)) == NULL) err(1, NULL); if (mft_parse_econtent(fn, mft, cms, cmsz) == 0) goto out; if ((cert = cert_parse_ee_cert(fn, talid, *x509)) == NULL) goto out; if (mft->signtime > mft->nextupdate) { warnx("%s: dating issue: CMS signing-time after MFT nextUpdate", fn); goto out; } rc = 1; out: if (rc == 0) { mft_free(mft); mft = NULL; X509_free(*x509); *x509 = NULL; } free(crldp); cert_free(cert); free(cms); return mft; } /* * Free an MFT pointer. * Safe to call with NULL. */ void mft_free(struct mft *p) { size_t i; if (p == NULL) return; for (i = 0; i < p->filesz; i++) free(p->files[i].file); free(p->path); free(p->files); free(p->seqnum); free(p->aia); free(p->aki); free(p->sia); free(p->ski); free(p->crl); free(p); } /* * Serialise MFT parsed content into the given buffer. * See mft_read() for the other side of the pipe. */ void mft_buffer(struct ibuf *b, const struct mft *p) { size_t i; io_simple_buffer(b, &p->repoid, sizeof(p->repoid)); io_simple_buffer(b, &p->talid, sizeof(p->talid)); io_simple_buffer(b, &p->certid, sizeof(p->certid)); io_str_buffer(b, p->path); io_str_buffer(b, p->aia); io_str_buffer(b, p->aki); io_str_buffer(b, p->ski); io_simple_buffer(b, &p->filesz, sizeof(size_t)); for (i = 0; i < p->filesz; i++) { io_str_buffer(b, p->files[i].file); io_simple_buffer(b, &p->files[i].type, sizeof(p->files[i].type)); io_simple_buffer(b, &p->files[i].location, sizeof(p->files[i].location)); io_simple_buffer(b, p->files[i].hash, SHA256_DIGEST_LENGTH); } } /* * Read an MFT structure from the file descriptor. * Result must be passed to mft_free(). */ struct mft * mft_read(struct ibuf *b) { struct mft *p = NULL; size_t i; if ((p = calloc(1, sizeof(struct mft))) == NULL) err(1, NULL); io_read_buf(b, &p->repoid, sizeof(p->repoid)); io_read_buf(b, &p->talid, sizeof(p->talid)); io_read_buf(b, &p->certid, sizeof(p->certid)); io_read_str(b, &p->path); io_read_str(b, &p->aia); io_read_str(b, &p->aki); io_read_str(b, &p->ski); assert(p->aia && p->aki && p->ski); io_read_buf(b, &p->filesz, sizeof(size_t)); if ((p->files = calloc(p->filesz, sizeof(struct mftfile))) == NULL) err(1, NULL); for (i = 0; i < p->filesz; i++) { io_read_str(b, &p->files[i].file); io_read_buf(b, &p->files[i].type, sizeof(p->files[i].type)); io_read_buf(b, &p->files[i].location, sizeof(p->files[i].location)); io_read_buf(b, p->files[i].hash, SHA256_DIGEST_LENGTH); } return p; } /* * Compare the thisupdate time of two mft files. */ int mft_compare_issued(const struct mft *a, const struct mft *b) { if (a->thisupdate > b->thisupdate) return 1; if (a->thisupdate < b->thisupdate) return -1; return 0; } /* * Compare the manifestNumber of two mft files. */ int mft_compare_seqnum(const struct mft *a, const struct mft *b) { int r; r = strlen(a->seqnum) - strlen(b->seqnum); if (r > 0) /* seqnum in a is longer -> higher */ return 1; if (r < 0) /* seqnum in a is shorter -> smaller */ return -1; r = strcmp(a->seqnum, b->seqnum); if (r > 0) /* a is greater, prefer a */ return 1; if (r < 0) /* b is greater, prefer b */ return -1; return 0; }