mft.c revision 1.18
1/*	$OpenBSD: mft.c,v 1.18 2020/11/05 16:38:09 tb Exp $ */
2/*
3 * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv>
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 */
17
18#include <assert.h>
19#include <err.h>
20#include <stdarg.h>
21#include <stdint.h>
22#include <fcntl.h>
23#include <stdlib.h>
24#include <string.h>
25#include <unistd.h>
26
27#include <openssl/sha.h>
28
29#include "extern.h"
30
31/*
32 * Parse results and data of the manifest file.
33 */
34struct	parse {
35	const char	*fn; /* manifest file name */
36	struct mft	*res; /* result object */
37};
38
39static const char *
40gentime2str(const ASN1_GENERALIZEDTIME *time)
41{
42	static char	buf[64];
43	BIO		*mem;
44
45	if ((mem = BIO_new(BIO_s_mem())) == NULL)
46		cryptoerrx("BIO_new");
47	if (!ASN1_GENERALIZEDTIME_print(mem, time))
48		cryptoerrx("ASN1_GENERALIZEDTIME_print");
49	if (BIO_gets(mem, buf, sizeof(buf)) < 0)
50		cryptoerrx("BIO_gets");
51
52	BIO_free(mem);
53	return buf;
54}
55
56/*
57 * Convert an ASN1_GENERALIZEDTIME to a struct tm.
58 * Returns 1 on success, 0 on failure.
59 */
60static int
61generalizedtime_to_tm(const ASN1_GENERALIZEDTIME *gtime, struct tm *tm)
62{
63	const char *data;
64	size_t len;
65
66	data = ASN1_STRING_get0_data(gtime);
67	len = ASN1_STRING_length(gtime);
68
69	return ASN1_time_parse(data, len, tm, V_ASN1_GENERALIZEDTIME) ==
70	    V_ASN1_GENERALIZEDTIME;
71}
72
73/*
74 * Validate and verify the time validity of the mft.
75 * Returns 1 if all is good, 0 if mft is stale, any other case -1.
76 */
77static int
78check_validity(const ASN1_GENERALIZEDTIME *from,
79    const ASN1_GENERALIZEDTIME *until, const char *fn)
80{
81	time_t now = time(NULL);
82	struct tm tm_from, tm_until, tm_now;
83
84	if (gmtime_r(&now, &tm_now) == NULL) {
85		warnx("%s: could not get current time", fn);
86		return -1;
87	}
88
89	if (!generalizedtime_to_tm(from, &tm_from)) {
90		warnx("%s: embedded from time format invalid", fn);
91		return -1;
92	}
93	if (!generalizedtime_to_tm(until, &tm_until)) {
94		warnx("%s: embedded until time format invalid", fn);
95		return -1;
96	}
97
98	/* check that until is not before from */
99	if (ASN1_time_tm_cmp(&tm_until, &tm_from) < 0) {
100		warnx("%s: bad update interval", fn);
101		return -1;
102	}
103	/* check that now is not before from */
104	if (ASN1_time_tm_cmp(&tm_from, &tm_now) > 0) {
105		warnx("%s: mft not yet valid %s", fn, gentime2str(from));
106		return -1;
107	}
108	/* check that now is not after until */
109	if (ASN1_time_tm_cmp(&tm_until, &tm_now) < 0) {
110		warnx("%s: mft expired on %s", fn, gentime2str(until));
111		return 0;
112	}
113
114	return 1;
115}
116
117/*
118 * Parse an individual "FileAndHash", RFC 6486, sec. 4.2.
119 * Return zero on failure, non-zero on success.
120 */
121static int
122mft_parse_filehash(struct parse *p, const ASN1_OCTET_STRING *os)
123{
124	ASN1_SEQUENCE_ANY	*seq;
125	const ASN1_TYPE		*file, *hash;
126	char			*fn = NULL;
127	const unsigned char	*d = os->data;
128	size_t			 dsz = os->length, sz;
129	int			 rc = 0;
130	struct mftfile		*fent;
131
132	if ((seq = d2i_ASN1_SEQUENCE_ANY(NULL, &d, dsz)) == NULL) {
133		cryptowarnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
134		    "failed ASN.1 sequence parse", p->fn);
135		goto out;
136	} else if (sk_ASN1_TYPE_num(seq) != 2) {
137		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
138		    "want 2 elements, have %d", p->fn,
139		    sk_ASN1_TYPE_num(seq));
140		goto out;
141	}
142
143	/* First is the filename itself. */
144
145	file = sk_ASN1_TYPE_value(seq, 0);
146	if (file->type != V_ASN1_IA5STRING) {
147		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
148		    "want ASN.1 IA5 string, have %s (NID %d)",
149		    p->fn, ASN1_tag2str(file->type), file->type);
150		goto out;
151	}
152	fn = strndup((const char *)file->value.ia5string->data,
153	    file->value.ia5string->length);
154	if (fn == NULL)
155		err(1, NULL);
156
157	/*
158	 * Make sure we're just a pathname and either an ROA or CER.
159	 * I don't think that the RFC specifically mentions this, but
160	 * it's in practical use and would really screw things up
161	 * (arbitrary filenames) otherwise.
162	 */
163
164	if (strchr(fn, '/') != NULL) {
165		warnx("%s: path components disallowed in filename: %s",
166		    p->fn, fn);
167		goto out;
168	} else if ((sz = strlen(fn)) <= 4) {
169		warnx("%s: filename must be large enough for suffix part: %s",
170		    p->fn, fn);
171		goto out;
172	}
173
174	if (strcasecmp(fn + sz - 4, ".roa") &&
175	    strcasecmp(fn + sz - 4, ".crl") &&
176	    strcasecmp(fn + sz - 4, ".cer")) {
177		/* ignore unknown files */
178		free(fn);
179		fn = NULL;
180		rc = 1;
181		goto out;
182	}
183
184	/* Now hash value. */
185
186	hash = sk_ASN1_TYPE_value(seq, 1);
187	if (hash->type != V_ASN1_BIT_STRING) {
188		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
189		    "want ASN.1 bit string, have %s (NID %d)",
190		    p->fn, ASN1_tag2str(hash->type), hash->type);
191		goto out;
192	}
193
194	if (hash->value.bit_string->length != SHA256_DIGEST_LENGTH) {
195		warnx("%s: RFC 6486 section 4.2.1: hash: "
196		    "invalid SHA256 length, have %d",
197		    p->fn, hash->value.bit_string->length);
198		goto out;
199	}
200
201	/* Insert the filename and hash value. */
202
203	p->res->files = reallocarray(p->res->files, p->res->filesz + 1,
204	    sizeof(struct mftfile));
205	if (p->res->files == NULL)
206		err(1, NULL);
207
208	fent = &p->res->files[p->res->filesz++];
209	memset(fent, 0, sizeof(struct mftfile));
210
211	fent->file = fn;
212	fn = NULL;
213	memcpy(fent->hash, hash->value.bit_string->data, SHA256_DIGEST_LENGTH);
214
215	rc = 1;
216out:
217	free(fn);
218	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
219	return rc;
220}
221
222/*
223 * Parse the "FileAndHash" sequence, RFC 6486, sec. 4.2.
224 * Return zero on failure, non-zero on success.
225 */
226static int
227mft_parse_flist(struct parse *p, const ASN1_OCTET_STRING *os)
228{
229	ASN1_SEQUENCE_ANY	*seq;
230	const ASN1_TYPE		*t;
231	const unsigned char	*d = os->data;
232	size_t			 dsz = os->length;
233	int			 i, rc = 0;
234
235	if ((seq = d2i_ASN1_SEQUENCE_ANY(NULL, &d, dsz)) == NULL) {
236		cryptowarnx("%s: RFC 6486 section 4.2: fileList: "
237		    "failed ASN.1 sequence parse", p->fn);
238		goto out;
239	}
240
241	for (i = 0; i < sk_ASN1_TYPE_num(seq); i++) {
242		t = sk_ASN1_TYPE_value(seq, i);
243		if (t->type != V_ASN1_SEQUENCE) {
244			warnx("%s: RFC 6486 section 4.2: fileList: "
245			    "want ASN.1 sequence, have %s (NID %d)",
246			    p->fn, ASN1_tag2str(t->type), t->type);
247			goto out;
248		} else if (!mft_parse_filehash(p, t->value.octet_string))
249			goto out;
250	}
251
252	rc = 1;
253out:
254	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
255	return rc;
256}
257
258/*
259 * Handle the eContent of the manifest object, RFC 6486 sec. 4.2.
260 * Returns <0 on failure, 0 on stale, >0 on success.
261 */
262static int
263mft_parse_econtent(const unsigned char *d, size_t dsz, struct parse *p)
264{
265	ASN1_SEQUENCE_ANY	*seq;
266	const ASN1_TYPE		*t;
267	const ASN1_GENERALIZEDTIME *from, *until;
268	int			 i, rc = -1, validity;
269
270	if ((seq = d2i_ASN1_SEQUENCE_ANY(NULL, &d, dsz)) == NULL) {
271		cryptowarnx("%s: RFC 6486 section 4.2: Manifest: "
272		    "failed ASN.1 sequence parse", p->fn);
273		goto out;
274	}
275
276	/* The version is optional. */
277
278	if (sk_ASN1_TYPE_num(seq) != 5 &&
279	    sk_ASN1_TYPE_num(seq) != 6) {
280		warnx("%s: RFC 6486 section 4.2: Manifest: "
281		    "want 5 or 6 elements, have %d", p->fn,
282		    sk_ASN1_TYPE_num(seq));
283		goto out;
284	}
285
286	/* Start with optional version. */
287
288	i = 0;
289	if (sk_ASN1_TYPE_num(seq) == 6) {
290		t = sk_ASN1_TYPE_value(seq, i++);
291		if (t->type != V_ASN1_INTEGER) {
292			warnx("%s: RFC 6486 section 4.2.1: version: "
293			    "want ASN.1 integer, have %s (NID %d)",
294			    p->fn, ASN1_tag2str(t->type), t->type);
295			goto out;
296		}
297	}
298
299	/* Now the manifest sequence number. */
300
301	t = sk_ASN1_TYPE_value(seq, i++);
302	if (t->type != V_ASN1_INTEGER) {
303		warnx("%s: RFC 6486 section 4.2.1: manifestNumber: "
304		    "want ASN.1 integer, have %s (NID %d)",
305		    p->fn, ASN1_tag2str(t->type), t->type);
306		goto out;
307	}
308
309	/*
310	 * Timestamps: this and next update time.
311	 * Validate that the current date falls into this interval.
312	 * This is required by section 4.4, (3).
313	 * If we're after the given date, then the MFT is stale.
314	 * This is made super complicated because it uses OpenSSL's
315	 * ASN1_GENERALIZEDTIME instead of ASN1_TIME, which we could
316	 * compare against the current time trivially.
317	 */
318
319	t = sk_ASN1_TYPE_value(seq, i++);
320	if (t->type != V_ASN1_GENERALIZEDTIME) {
321		warnx("%s: RFC 6486 section 4.2.1: thisUpdate: "
322		    "want ASN.1 generalised time, have %s (NID %d)",
323		    p->fn, ASN1_tag2str(t->type), t->type);
324		goto out;
325	}
326	from = t->value.generalizedtime;
327
328	t = sk_ASN1_TYPE_value(seq, i++);
329	if (t->type != V_ASN1_GENERALIZEDTIME) {
330		warnx("%s: RFC 6486 section 4.2.1: nextUpdate: "
331		    "want ASN.1 generalised time, have %s (NID %d)",
332		    p->fn, ASN1_tag2str(t->type), t->type);
333		goto out;
334	}
335	until = t->value.generalizedtime;
336
337	validity = check_validity(from, until, p->fn);
338	if (validity != 1)
339		goto out;
340
341	/* File list algorithm. */
342
343	t = sk_ASN1_TYPE_value(seq, i++);
344	if (t->type != V_ASN1_OBJECT) {
345		warnx("%s: RFC 6486 section 4.2.1: fileHashAlg: "
346		    "want ASN.1 object time, have %s (NID %d)",
347		    p->fn, ASN1_tag2str(t->type), t->type);
348		goto out;
349	} else if (OBJ_obj2nid(t->value.object) != NID_sha256) {
350		warnx("%s: RFC 6486 section 4.2.1: fileHashAlg: "
351		    "want SHA256 object, have %s (NID %d)", p->fn,
352		    ASN1_tag2str(OBJ_obj2nid(t->value.object)),
353		    OBJ_obj2nid(t->value.object));
354		goto out;
355	}
356
357	/* Now the sequence. */
358
359	t = sk_ASN1_TYPE_value(seq, i++);
360	if (t->type != V_ASN1_SEQUENCE) {
361		warnx("%s: RFC 6486 section 4.2.1: fileList: "
362		    "want ASN.1 sequence, have %s (NID %d)",
363		    p->fn, ASN1_tag2str(t->type), t->type);
364		goto out;
365	} else if (!mft_parse_flist(p, t->value.octet_string))
366		goto out;
367
368	rc = validity;
369out:
370	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
371	return rc;
372}
373
374/*
375 * Parse the objects that have been published in the manifest.
376 * This conforms to RFC 6486.
377 * Note that if the MFT is stale, all referenced objects are stripped
378 * from the parsed content.
379 * The MFT content is otherwise returned.
380 */
381struct mft *
382mft_parse(X509 **x509, const char *fn)
383{
384	struct parse	 p;
385	int		 c, rc = 0;
386	size_t		 i, cmsz;
387	unsigned char	*cms;
388
389	memset(&p, 0, sizeof(struct parse));
390	p.fn = fn;
391
392	cms = cms_parse_validate(x509, fn, "1.2.840.113549.1.9.16.1.26",
393	    NULL, &cmsz);
394	if (cms == NULL)
395		return NULL;
396	assert(*x509 != NULL);
397
398	if ((p.res = calloc(1, sizeof(struct mft))) == NULL)
399		err(1, NULL);
400	if ((p.res->file = strdup(fn)) == NULL)
401		err(1, NULL);
402	if (!x509_get_ski_aki(*x509, fn, &p.res->ski, &p.res->aki))
403		goto out;
404
405	/*
406	 * If we're stale, then remove all of the files that the MFT
407	 * references as well as marking it as stale.
408	 */
409
410	if ((c = mft_parse_econtent(cms, cmsz, &p)) == 0) {
411		/*
412		 * FIXME: it should suffice to just mark this as stale
413		 * and have the logic around mft_read() simply ignore
414		 * the contents of stale entries, just like it does for
415		 * invalid ROAs or certificates.
416		 */
417
418		p.res->stale = 1;
419		if (p.res->files != NULL)
420			for (i = 0; i < p.res->filesz; i++)
421				free(p.res->files[i].file);
422		free(p.res->files);
423		p.res->filesz = 0;
424		p.res->files = NULL;
425	} else if (c == -1)
426		goto out;
427
428	rc = 1;
429out:
430	if (rc == 0) {
431		mft_free(p.res);
432		p.res = NULL;
433		X509_free(*x509);
434		*x509 = NULL;
435	}
436	free(cms);
437	return p.res;
438}
439
440/*
441 * Check the hash value of a file.
442 * Return zero on failure, non-zero on success.
443 */
444static int
445mft_validfilehash(const char *fn, const struct mftfile *m)
446{
447	char	filehash[SHA256_DIGEST_LENGTH];
448	char	buffer[8192];
449	char	*cp, *path = NULL;
450	SHA256_CTX ctx;
451	ssize_t	nr;
452	int	fd;
453
454	/* Check hash of file now, but first build path for it */
455	cp = strrchr(fn, '/');
456	assert(cp != NULL);
457	if (asprintf(&path, "%.*s/%s", (int)(cp - fn), fn, m->file) == -1)
458		err(1, "asprintf");
459
460	if ((fd = open(path, O_RDONLY)) == -1) {
461		warn("%s: referenced file %s", fn, m->file);
462		free(path);
463		return 0;
464	}
465	free(path);
466
467	SHA256_Init(&ctx);
468	while ((nr = read(fd, buffer, sizeof(buffer))) > 0) {
469		SHA256_Update(&ctx, buffer, nr);
470	}
471	close(fd);
472
473	SHA256_Final(filehash, &ctx);
474	if (memcmp(m->hash, filehash, SHA256_DIGEST_LENGTH) != 0) {
475		warnx("%s: bad message digest for %s", fn, m->file);
476		return 0;
477	}
478
479	return 1;
480}
481
482/*
483 * Check all files and their hashes in a MFT structure.
484 * Return zero on failure, non-zero on success.
485 */
486int
487mft_check(const char *fn, struct mft *p)
488{
489	size_t	i;
490	int	rc = 1;
491
492	for (i = 0; i < p->filesz; i++)
493		if (!mft_validfilehash(fn, &p->files[i]))
494			rc = 0;
495
496	return rc;
497}
498
499/*
500 * Free an MFT pointer.
501 * Safe to call with NULL.
502 */
503void
504mft_free(struct mft *p)
505{
506	size_t	 i;
507
508	if (p == NULL)
509		return;
510
511	if (p->files != NULL)
512		for (i = 0; i < p->filesz; i++)
513			free(p->files[i].file);
514
515	free(p->aki);
516	free(p->ski);
517	free(p->file);
518	free(p->files);
519	free(p);
520}
521
522/*
523 * Serialise MFT parsed content into the given buffer.
524 * See mft_read() for the other side of the pipe.
525 */
526void
527mft_buffer(char **b, size_t *bsz, size_t *bmax, const struct mft *p)
528{
529	size_t		 i;
530
531	io_simple_buffer(b, bsz, bmax, &p->stale, sizeof(int));
532	io_str_buffer(b, bsz, bmax, p->file);
533	io_simple_buffer(b, bsz, bmax, &p->filesz, sizeof(size_t));
534
535	for (i = 0; i < p->filesz; i++) {
536		io_str_buffer(b, bsz, bmax, p->files[i].file);
537		io_simple_buffer(b, bsz, bmax,
538			p->files[i].hash, SHA256_DIGEST_LENGTH);
539	}
540
541	io_str_buffer(b, bsz, bmax, p->aki);
542	io_str_buffer(b, bsz, bmax, p->ski);
543}
544
545/*
546 * Read an MFT structure from the file descriptor.
547 * Result must be passed to mft_free().
548 */
549struct mft *
550mft_read(int fd)
551{
552	struct mft	*p = NULL;
553	size_t		 i;
554
555	if ((p = calloc(1, sizeof(struct mft))) == NULL)
556		err(1, NULL);
557
558	io_simple_read(fd, &p->stale, sizeof(int));
559	io_str_read(fd, &p->file);
560	io_simple_read(fd, &p->filesz, sizeof(size_t));
561
562	if ((p->files = calloc(p->filesz, sizeof(struct mftfile))) == NULL)
563		err(1, NULL);
564
565	for (i = 0; i < p->filesz; i++) {
566		io_str_read(fd, &p->files[i].file);
567		io_simple_read(fd, p->files[i].hash, SHA256_DIGEST_LENGTH);
568	}
569
570	io_str_read(fd, &p->aki);
571	io_str_read(fd, &p->ski);
572	return p;
573}
574