mft.c revision 1.98
1/*	$OpenBSD: mft.c,v 1.98 2023/09/25 11:08:45 tb Exp $ */
2/*
3 * Copyright (c) 2022 Theo Buehler <tb@openbsd.org>
4 * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv>
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19#include <assert.h>
20#include <err.h>
21#include <limits.h>
22#include <stdint.h>
23#include <stdlib.h>
24#include <string.h>
25#include <unistd.h>
26
27#include <openssl/bn.h>
28#include <openssl/asn1.h>
29#include <openssl/asn1t.h>
30#include <openssl/safestack.h>
31#include <openssl/sha.h>
32#include <openssl/stack.h>
33#include <openssl/x509.h>
34
35#include "extern.h"
36
37/*
38 * Parse results and data of the manifest file.
39 */
40struct	parse {
41	const char	*fn; /* manifest file name */
42	struct mft	*res; /* result object */
43	int		 found_crl;
44};
45
46extern ASN1_OBJECT	*mft_oid;
47
48/*
49 * Types and templates for the Manifest eContent, RFC 6486, section 4.2.
50 */
51
52typedef struct {
53	ASN1_IA5STRING	*file;
54	ASN1_BIT_STRING	*hash;
55} FileAndHash;
56
57DECLARE_STACK_OF(FileAndHash);
58
59#ifndef DEFINE_STACK_OF
60#define sk_FileAndHash_num(sk)		SKM_sk_num(FileAndHash, (sk))
61#define sk_FileAndHash_value(sk, i)	SKM_sk_value(FileAndHash, (sk), (i))
62#endif
63
64typedef struct {
65	ASN1_INTEGER		*version;
66	ASN1_INTEGER		*manifestNumber;
67	ASN1_GENERALIZEDTIME	*thisUpdate;
68	ASN1_GENERALIZEDTIME	*nextUpdate;
69	ASN1_OBJECT		*fileHashAlg;
70	STACK_OF(FileAndHash)	*fileList;
71} Manifest;
72
73ASN1_SEQUENCE(FileAndHash) = {
74	ASN1_SIMPLE(FileAndHash, file, ASN1_IA5STRING),
75	ASN1_SIMPLE(FileAndHash, hash, ASN1_BIT_STRING),
76} ASN1_SEQUENCE_END(FileAndHash);
77
78ASN1_SEQUENCE(Manifest) = {
79	ASN1_EXP_OPT(Manifest, version, ASN1_INTEGER, 0),
80	ASN1_SIMPLE(Manifest, manifestNumber, ASN1_INTEGER),
81	ASN1_SIMPLE(Manifest, thisUpdate, ASN1_GENERALIZEDTIME),
82	ASN1_SIMPLE(Manifest, nextUpdate, ASN1_GENERALIZEDTIME),
83	ASN1_SIMPLE(Manifest, fileHashAlg, ASN1_OBJECT),
84	ASN1_SEQUENCE_OF(Manifest, fileList, FileAndHash),
85} ASN1_SEQUENCE_END(Manifest);
86
87DECLARE_ASN1_FUNCTIONS(Manifest);
88IMPLEMENT_ASN1_FUNCTIONS(Manifest);
89
90#define GENTIME_LENGTH 15
91
92/*
93 * Convert an ASN1_GENERALIZEDTIME to a struct tm.
94 * Returns 1 on success, 0 on failure.
95 */
96static int
97generalizedtime_to_tm(const ASN1_GENERALIZEDTIME *gtime, struct tm *tm)
98{
99	/*
100	 * ASN1_GENERALIZEDTIME is another name for ASN1_STRING. Check type and
101	 * length, so we don't accidentally accept a UTCTime. Punt on checking
102	 * Zulu time for OpenSSL: we don't want to mess about with silly flags.
103	 */
104	if (ASN1_STRING_type(gtime) != V_ASN1_GENERALIZEDTIME)
105		return 0;
106	if (ASN1_STRING_length(gtime) != GENTIME_LENGTH)
107		return 0;
108
109	memset(tm, 0, sizeof(*tm));
110	return ASN1_TIME_to_tm(gtime, tm);
111}
112
113/*
114 * Validate and verify the time validity of the mft.
115 * Returns 1 if all is good and for any other case 0.
116 */
117static int
118mft_parse_time(const ASN1_GENERALIZEDTIME *from,
119    const ASN1_GENERALIZEDTIME *until, struct parse *p)
120{
121	struct tm tm_from, tm_until;
122
123	if (!generalizedtime_to_tm(from, &tm_from)) {
124		warnx("%s: embedded from time format invalid", p->fn);
125		return 0;
126	}
127	if (!generalizedtime_to_tm(until, &tm_until)) {
128		warnx("%s: embedded until time format invalid", p->fn);
129		return 0;
130	}
131
132	if ((p->res->thisupdate = timegm(&tm_from)) == -1 ||
133	    (p->res->nextupdate = timegm(&tm_until)) == -1)
134		errx(1, "%s: timegm failed", p->fn);
135
136	if (p->res->thisupdate > p->res->nextupdate) {
137		warnx("%s: bad update interval", p->fn);
138		return 0;
139	}
140
141	return 1;
142}
143
144/*
145 * Determine rtype corresponding to file extension. Returns RTYPE_INVALID
146 * on error or unkown extension.
147 */
148enum rtype
149rtype_from_file_extension(const char *fn)
150{
151	size_t	 sz;
152
153	sz = strlen(fn);
154	if (sz < 5)
155		return RTYPE_INVALID;
156
157	if (strcasecmp(fn + sz - 4, ".tal") == 0)
158		return RTYPE_TAL;
159	if (strcasecmp(fn + sz - 4, ".cer") == 0)
160		return RTYPE_CER;
161	if (strcasecmp(fn + sz - 4, ".crl") == 0)
162		return RTYPE_CRL;
163	if (strcasecmp(fn + sz - 4, ".mft") == 0)
164		return RTYPE_MFT;
165	if (strcasecmp(fn + sz - 4, ".roa") == 0)
166		return RTYPE_ROA;
167	if (strcasecmp(fn + sz - 4, ".gbr") == 0)
168		return RTYPE_GBR;
169	if (strcasecmp(fn + sz - 4, ".sig") == 0)
170		return RTYPE_RSC;
171	if (strcasecmp(fn + sz - 4, ".asa") == 0)
172		return RTYPE_ASPA;
173	if (strcasecmp(fn + sz - 4, ".tak") == 0)
174		return RTYPE_TAK;
175	if (strcasecmp(fn + sz - 4, ".csv") == 0)
176		return RTYPE_GEOFEED;
177
178	return RTYPE_INVALID;
179}
180
181/*
182 * Validate that a filename listed on a Manifest only contains characters
183 * permitted in draft-ietf-sidrops-6486bis section 4.2.2
184 * Also ensure that there is exactly one '.'.
185 */
186static int
187valid_mft_filename(const char *fn, size_t len)
188{
189	const unsigned char *c;
190
191	if (!valid_filename(fn, len))
192		return 0;
193
194	c = memchr(fn, '.', len);
195	if (c == NULL || c != memrchr(fn, '.', len))
196		return 0;
197
198	return 1;
199}
200
201/*
202 * Check that the file is allowed to be part of a manifest and the parser
203 * for this type is implemented in rpki-client.
204 * Returns corresponding rtype or RTYPE_INVALID to mark the file as unknown.
205 */
206static enum rtype
207rtype_from_mftfile(const char *fn)
208{
209	enum rtype		 type;
210
211	type = rtype_from_file_extension(fn);
212	switch (type) {
213	case RTYPE_CER:
214	case RTYPE_CRL:
215	case RTYPE_GBR:
216	case RTYPE_ROA:
217	case RTYPE_ASPA:
218	case RTYPE_TAK:
219		return type;
220	default:
221		return RTYPE_INVALID;
222	}
223}
224
225/*
226 * Parse an individual "FileAndHash", RFC 6486, sec. 4.2.
227 * Return zero on failure, non-zero on success.
228 */
229static int
230mft_parse_filehash(struct parse *p, const FileAndHash *fh)
231{
232	char			*fn = NULL;
233	int			 rc = 0;
234	struct mftfile		*fent;
235	enum rtype		 type;
236	size_t			 new_idx = 0;
237
238	if (!valid_mft_filename(fh->file->data, fh->file->length)) {
239		warnx("%s: RFC 6486 section 4.2.2: bad filename", p->fn);
240		goto out;
241	}
242	fn = strndup(fh->file->data, fh->file->length);
243	if (fn == NULL)
244		err(1, NULL);
245
246	if (fh->hash->length != SHA256_DIGEST_LENGTH) {
247		warnx("%s: RFC 6486 section 4.2.1: hash: "
248		    "invalid SHA256 length, have %d",
249		    p->fn, fh->hash->length);
250		goto out;
251	}
252
253	type = rtype_from_mftfile(fn);
254	/* remember the filehash for the CRL in struct mft */
255	if (type == RTYPE_CRL && strcmp(fn, p->res->crl) == 0) {
256		memcpy(p->res->crlhash, fh->hash->data, SHA256_DIGEST_LENGTH);
257		p->found_crl = 1;
258	}
259
260	if (filemode)
261		fent = &p->res->files[p->res->filesz++];
262	else {
263		/* Fisher-Yates shuffle */
264		new_idx = arc4random_uniform(p->res->filesz + 1);
265		p->res->files[p->res->filesz++] = p->res->files[new_idx];
266		fent = &p->res->files[new_idx];
267	}
268
269	fent->type = type;
270	fent->file = fn;
271	fn = NULL;
272	memcpy(fent->hash, fh->hash->data, SHA256_DIGEST_LENGTH);
273
274	rc = 1;
275 out:
276	free(fn);
277	return rc;
278}
279
280/*
281 * Handle the eContent of the manifest object, RFC 6486 sec. 4.2.
282 * Returns 0 on failure and 1 on success.
283 */
284static int
285mft_parse_econtent(const unsigned char *d, size_t dsz, struct parse *p)
286{
287	Manifest		*mft;
288	FileAndHash		*fh;
289	int			 i, rc = 0;
290
291	if ((mft = d2i_Manifest(NULL, &d, dsz)) == NULL) {
292		warnx("%s: RFC 6486 section 4: failed to parse Manifest",
293		    p->fn);
294		goto out;
295	}
296
297	if (!valid_econtent_version(p->fn, mft->version, 0))
298		goto out;
299
300	p->res->seqnum = x509_convert_seqnum(p->fn, mft->manifestNumber);
301	if (p->res->seqnum == NULL)
302		goto out;
303
304	/*
305	 * Timestamps: this and next update time.
306	 * Validate that the current date falls into this interval.
307	 * This is required by section 4.4, (3).
308	 * If we're after the given date, then the MFT is stale.
309	 * This is made super complicated because it uses OpenSSL's
310	 * ASN1_GENERALIZEDTIME instead of ASN1_TIME, which we could
311	 * compare against the current time trivially.
312	 */
313
314	if (!mft_parse_time(mft->thisUpdate, mft->nextUpdate, p))
315		goto out;
316
317	if (OBJ_obj2nid(mft->fileHashAlg) != NID_sha256) {
318		warnx("%s: RFC 6486 section 4.2.1: fileHashAlg: "
319		    "want SHA256 object, have %s (NID %d)", p->fn,
320		    ASN1_tag2str(OBJ_obj2nid(mft->fileHashAlg)),
321		    OBJ_obj2nid(mft->fileHashAlg));
322		goto out;
323	}
324
325	if (sk_FileAndHash_num(mft->fileList) >= MAX_MANIFEST_ENTRIES) {
326		warnx("%s: %d exceeds manifest entry limit (%d)", p->fn,
327		    sk_FileAndHash_num(mft->fileList), MAX_MANIFEST_ENTRIES);
328		goto out;
329	}
330
331	p->res->files = calloc(sk_FileAndHash_num(mft->fileList),
332	    sizeof(struct mftfile));
333	if (p->res->files == NULL)
334		err(1, NULL);
335
336	for (i = 0; i < sk_FileAndHash_num(mft->fileList); i++) {
337		fh = sk_FileAndHash_value(mft->fileList, i);
338		if (!mft_parse_filehash(p, fh))
339			goto out;
340	}
341
342	if (!p->found_crl) {
343		warnx("%s: CRL not part of MFT fileList", p->fn);
344		goto out;
345	}
346
347	rc = 1;
348 out:
349	Manifest_free(mft);
350	return rc;
351}
352
353/*
354 * Parse the objects that have been published in the manifest.
355 * This conforms to RFC 6486.
356 * Note that if the MFT is stale, all referenced objects are stripped
357 * from the parsed content.
358 * The MFT content is otherwise returned.
359 */
360struct mft *
361mft_parse(X509 **x509, const char *fn, int talid, const unsigned char *der,
362    size_t len)
363{
364	struct parse	 p;
365	struct cert	*cert = NULL;
366	int		 rc = 0;
367	size_t		 cmsz;
368	unsigned char	*cms;
369	char		*crldp = NULL, *crlfile;
370	time_t		 signtime = 0;
371
372	memset(&p, 0, sizeof(struct parse));
373	p.fn = fn;
374
375	cms = cms_parse_validate(x509, fn, der, len, mft_oid, &cmsz, &signtime);
376	if (cms == NULL)
377		return NULL;
378	assert(*x509 != NULL);
379
380	if ((p.res = calloc(1, sizeof(struct mft))) == NULL)
381		err(1, NULL);
382	p.res->signtime = signtime;
383
384	if (!x509_get_aia(*x509, fn, &p.res->aia))
385		goto out;
386	if (!x509_get_aki(*x509, fn, &p.res->aki))
387		goto out;
388	if (!x509_get_sia(*x509, fn, &p.res->sia))
389		goto out;
390	if (!x509_get_ski(*x509, fn, &p.res->ski))
391		goto out;
392	if (p.res->aia == NULL || p.res->aki == NULL || p.res->sia == NULL ||
393	    p.res->ski == NULL) {
394		warnx("%s: RFC 6487 section 4.8: "
395		    "missing AIA, AKI, SIA, or SKI X509 extension", fn);
396		goto out;
397	}
398
399	if (!x509_inherits(*x509)) {
400		warnx("%s: RFC 3779 extension not set to inherit", fn);
401		goto out;
402	}
403
404	/* get CRL info for later */
405	if (!x509_get_crl(*x509, fn, &crldp))
406		goto out;
407	if (crldp == NULL) {
408		warnx("%s: RFC 6487 section 4.8.6: CRL: "
409		    "missing CRL distribution point extension", fn);
410		goto out;
411	}
412	crlfile = strrchr(crldp, '/');
413	if (crlfile == NULL) {
414		warnx("%s: RFC 6487 section 4.8.6: "
415		    "invalid CRL distribution point", fn);
416		goto out;
417	}
418	crlfile++;
419	if (!valid_mft_filename(crlfile, strlen(crlfile)) ||
420	    rtype_from_file_extension(crlfile) != RTYPE_CRL) {
421		warnx("%s: RFC 6487 section 4.8.6: CRL: "
422		    "bad CRL distribution point extension", fn);
423		goto out;
424	}
425	if ((p.res->crl = strdup(crlfile)) == NULL)
426		err(1, NULL);
427
428	if (mft_parse_econtent(cms, cmsz, &p) == 0)
429		goto out;
430
431	if ((cert = cert_parse_ee_cert(fn, *x509)) == NULL)
432		goto out;
433
434	if (p.res->signtime > p.res->nextupdate) {
435		warnx("%s: dating issue: CMS signing-time after MFT nextUpdate",
436		    fn);
437		goto out;
438	}
439
440	rc = 1;
441out:
442	if (rc == 0) {
443		mft_free(p.res);
444		p.res = NULL;
445		X509_free(*x509);
446		*x509 = NULL;
447	}
448	free(crldp);
449	cert_free(cert);
450	free(cms);
451	return p.res;
452}
453
454/*
455 * Free an MFT pointer.
456 * Safe to call with NULL.
457 */
458void
459mft_free(struct mft *p)
460{
461	size_t	 i;
462
463	if (p == NULL)
464		return;
465
466	if (p->files != NULL)
467		for (i = 0; i < p->filesz; i++)
468			free(p->files[i].file);
469
470	free(p->aia);
471	free(p->aki);
472	free(p->sia);
473	free(p->ski);
474	free(p->path);
475	free(p->files);
476	free(p->seqnum);
477	free(p);
478}
479
480/*
481 * Serialise MFT parsed content into the given buffer.
482 * See mft_read() for the other side of the pipe.
483 */
484void
485mft_buffer(struct ibuf *b, const struct mft *p)
486{
487	size_t		 i;
488
489	io_simple_buffer(b, &p->stale, sizeof(p->stale));
490	io_simple_buffer(b, &p->repoid, sizeof(p->repoid));
491	io_simple_buffer(b, &p->talid, sizeof(p->talid));
492	io_str_buffer(b, p->path);
493
494	io_str_buffer(b, p->aia);
495	io_str_buffer(b, p->aki);
496	io_str_buffer(b, p->ski);
497
498	io_simple_buffer(b, &p->filesz, sizeof(size_t));
499	for (i = 0; i < p->filesz; i++) {
500		io_str_buffer(b, p->files[i].file);
501		io_simple_buffer(b, &p->files[i].type,
502		    sizeof(p->files[i].type));
503		io_simple_buffer(b, &p->files[i].location,
504		    sizeof(p->files[i].location));
505		io_simple_buffer(b, p->files[i].hash, SHA256_DIGEST_LENGTH);
506	}
507}
508
509/*
510 * Read an MFT structure from the file descriptor.
511 * Result must be passed to mft_free().
512 */
513struct mft *
514mft_read(struct ibuf *b)
515{
516	struct mft	*p = NULL;
517	size_t		 i;
518
519	if ((p = calloc(1, sizeof(struct mft))) == NULL)
520		err(1, NULL);
521
522	io_read_buf(b, &p->stale, sizeof(p->stale));
523	io_read_buf(b, &p->repoid, sizeof(p->repoid));
524	io_read_buf(b, &p->talid, sizeof(p->talid));
525	io_read_str(b, &p->path);
526
527	io_read_str(b, &p->aia);
528	io_read_str(b, &p->aki);
529	io_read_str(b, &p->ski);
530	assert(p->aia && p->aki && p->ski);
531
532	io_read_buf(b, &p->filesz, sizeof(size_t));
533	if ((p->files = calloc(p->filesz, sizeof(struct mftfile))) == NULL)
534		err(1, NULL);
535
536	for (i = 0; i < p->filesz; i++) {
537		io_read_str(b, &p->files[i].file);
538		io_read_buf(b, &p->files[i].type, sizeof(p->files[i].type));
539		io_read_buf(b, &p->files[i].location,
540		    sizeof(p->files[i].location));
541		io_read_buf(b, p->files[i].hash, SHA256_DIGEST_LENGTH);
542	}
543
544	return p;
545}
546
547/*
548 * Compare two MFT files, returns 1 if first MFT is preferred and 0 if second
549 * MFT should be used.
550 */
551int
552mft_compare(const struct mft *a, const struct mft *b)
553{
554	int r;
555
556	if (b == NULL)
557		return 1;
558	if (a == NULL)
559		return 0;
560
561	r = strlen(a->seqnum) - strlen(b->seqnum);
562	if (r > 0)	/* seqnum in a is longer -> higher */
563		return 1;
564	if (r < 0)	/* seqnum in a is shorter -> smaller */
565		return 0;
566
567	r = strcmp(a->seqnum, b->seqnum);
568	if (r > 0)	/* a is greater, prefer a */
569		return 1;
570	return 0;
571}
572