mft.c revision 1.8
1/*	$OpenBSD: mft.c,v 1.8 2019/10/23 07:36:29 claudio 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 <stdlib.h>
23#include <string.h>
24#include <unistd.h>
25
26#include <openssl/ssl.h>
27
28#include "extern.h"
29
30/*
31 * Parse results and data of the manifest file.
32 */
33struct	parse {
34	const char	*fn; /* manifest file name */
35	struct mft	*res; /* result object */
36};
37
38static const char *
39gentime2str(const ASN1_GENERALIZEDTIME *time)
40{
41	static char	buf[64];
42	BIO		*mem;
43
44	if ((mem = BIO_new(BIO_s_mem())) == NULL)
45		cryptoerrx("BIO_new");
46	if (!ASN1_GENERALIZEDTIME_print(mem, time))
47		cryptoerrx("ASN1_GENERALIZEDTIME_print");
48	if (BIO_gets(mem, buf, sizeof(buf)) < 0)
49		cryptoerrx("BIO_gets");
50
51	BIO_free(mem);
52	return buf;
53}
54
55/*
56 * Validate and verify the time validity of the mft.
57 * Returns 1 if all is good, 0 if mft is stale, any other case -1.
58 * XXX should use ASN1_time_tm_cmp() once libressl is used.
59 */
60static time_t
61check_validity(const ASN1_GENERALIZEDTIME *from,
62    const ASN1_GENERALIZEDTIME *until, const char *fn, int force)
63{
64	time_t now = time(NULL);
65
66	if (!ASN1_GENERALIZEDTIME_check(from) ||
67	    !ASN1_GENERALIZEDTIME_check(until)) {
68		warnx("%s: embedded time format invalid", fn);
69		return -1;
70	}
71	/* check that until is not before from */
72	if (ASN1_STRING_cmp(until, from) < 0) {
73		warnx("%s: bad update interval", fn);
74		return -1;
75	}
76	/* check that now is not before from */
77	if (X509_cmp_time(from, &now) > 0) {
78		warnx("%s: mft not yet valid %s", fn, gentime2str(from));
79		return -1;
80	}
81	/* check that now is not after until */
82	if (X509_cmp_time(until, &now) < 0) {
83		warnx("%s: mft expired on %s%s", fn, gentime2str(until),
84		    force ? " (ignoring)" : "");
85		if (!force)
86			return 0;
87	}
88
89	return 1;
90}
91
92/*
93 * Parse an individual "FileAndHash", RFC 6486, sec. 4.2.
94 * Return zero on failure, non-zero on success.
95 */
96static int
97mft_parse_filehash(struct parse *p, const ASN1_OCTET_STRING *os)
98{
99	const ASN1_SEQUENCE_ANY *seq;
100	const ASN1_TYPE		*file, *hash;
101	char			*fn = NULL;
102	const unsigned char	*d = os->data;
103	size_t			 dsz = os->length, sz;
104	int			 rc = 0;
105	struct mftfile		*fent;
106
107	if ((seq = d2i_ASN1_SEQUENCE_ANY(NULL, &d, dsz)) == NULL) {
108		cryptowarnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
109		    "failed ASN.1 sequence parse", p->fn);
110		goto out;
111	} else if (sk_ASN1_TYPE_num(seq) != 2) {
112		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
113		    "want 2 elements, have %d", p->fn,
114		    sk_ASN1_TYPE_num(seq));
115		goto out;
116	}
117
118	/* First is the filename itself. */
119
120	file = sk_ASN1_TYPE_value(seq, 0);
121	if (file->type != V_ASN1_IA5STRING) {
122		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
123		    "want ASN.1 IA5 string, have %s (NID %d)",
124		    p->fn, ASN1_tag2str(file->type), file->type);
125		goto out;
126	}
127	fn = strndup((const char *)file->value.ia5string->data,
128	    file->value.ia5string->length);
129	if (fn == NULL)
130		err(EXIT_FAILURE, NULL);
131
132	/*
133	 * Make sure we're just a pathname and either an ROA or CER.
134	 * I don't think that the RFC specifically mentions this, but
135	 * it's in practical use and would really screw things up
136	 * (arbitrary filenames) otherwise.
137	 */
138
139	if (strchr(fn, '/') != NULL) {
140		warnx("%s: path components disallowed in filename: %s",
141		    p->fn, fn);
142		goto out;
143	} else if ((sz = strlen(fn)) <= 4) {
144		warnx("%s: filename must be large enough for suffix part: %s",
145		    p->fn, fn);
146		goto out;
147	}
148
149	if (strcasecmp(fn + sz - 4, ".roa") &&
150	    strcasecmp(fn + sz - 4, ".crl") &&
151	    strcasecmp(fn + sz - 4, ".cer")) {
152		free(fn);
153		fn = NULL;
154		rc = 1;
155		goto out;
156	}
157
158	/* Now hash value. */
159
160	hash = sk_ASN1_TYPE_value(seq, 1);
161	if (hash->type != V_ASN1_BIT_STRING) {
162		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
163		    "want ASN.1 bit string, have %s (NID %d)",
164		    p->fn, ASN1_tag2str(hash->type), hash->type);
165		goto out;
166	}
167
168	if (hash->value.bit_string->length != SHA256_DIGEST_LENGTH) {
169		warnx("%s: RFC 6486 section 4.2.1: hash: "
170		    "invalid SHA256 length, have %d",
171		    p->fn, hash->value.bit_string->length);
172		goto out;
173	}
174
175	/* Insert the filename and hash value. */
176
177	p->res->files = reallocarray(p->res->files, p->res->filesz + 1,
178	    sizeof(struct mftfile));
179	if (p->res->files == NULL)
180		err(EXIT_FAILURE, NULL);
181
182	fent = &p->res->files[p->res->filesz++];
183	memset(fent, 0, sizeof(struct mftfile));
184
185	fent->file = fn;
186	fn = NULL;
187	memcpy(fent->hash, hash->value.bit_string->data, SHA256_DIGEST_LENGTH);
188
189	rc = 1;
190out:
191	free(fn);
192	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
193	return rc;
194}
195
196/*
197 * Parse the "FileAndHash" sequence, RFC 6486, sec. 4.2.
198 * Return zero on failure, non-zero on success.
199 */
200static int
201mft_parse_flist(struct parse *p, const ASN1_OCTET_STRING *os)
202{
203	ASN1_SEQUENCE_ANY	*seq;
204	const ASN1_TYPE		*t;
205	const unsigned char	*d = os->data;
206	size_t			 dsz = os->length;
207	int			 i, rc = 0;
208
209	if ((seq = d2i_ASN1_SEQUENCE_ANY(NULL, &d, dsz)) == NULL) {
210		cryptowarnx("%s: RFC 6486 section 4.2: fileList: "
211		    "failed ASN.1 sequence parse", p->fn);
212		goto out;
213	}
214
215	for (i = 0; i < sk_ASN1_TYPE_num(seq); i++) {
216		t = sk_ASN1_TYPE_value(seq, i);
217		if (t->type != V_ASN1_SEQUENCE) {
218			warnx("%s: RFC 6486 section 4.2: fileList: "
219			    "want ASN.1 sequence, have %s (NID %d)",
220			    p->fn, ASN1_tag2str(t->type), t->type);
221			goto out;
222		} else if (!mft_parse_filehash(p, t->value.octet_string))
223			goto out;
224	}
225
226	rc = 1;
227out:
228	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
229	return rc;
230}
231
232/*
233 * Handle the eContent of the manifest object, RFC 6486 sec. 4.2.
234 * Returns <0 on failure, 0 on stale, >0 on success.
235 */
236static int
237mft_parse_econtent(const unsigned char *d, size_t dsz, struct parse *p, int force)
238{
239	ASN1_SEQUENCE_ANY	*seq;
240	const ASN1_TYPE		*t;
241	const ASN1_GENERALIZEDTIME *from, *until;
242	int			 i, rc = -1;
243
244	if ((seq = d2i_ASN1_SEQUENCE_ANY(NULL, &d, dsz)) == NULL) {
245		cryptowarnx("%s: RFC 6486 section 4.2: Manifest: "
246		    "failed ASN.1 sequence parse", p->fn);
247		goto out;
248	}
249
250	/* The version is optional. */
251
252	if (sk_ASN1_TYPE_num(seq) != 5 &&
253	    sk_ASN1_TYPE_num(seq) != 6) {
254		warnx("%s: RFC 6486 section 4.2: Manifest: "
255		    "want 5 or 6 elements, have %d", p->fn,
256		    sk_ASN1_TYPE_num(seq));
257		goto out;
258	}
259
260	/* Start with optional version. */
261
262	i = 0;
263	if (sk_ASN1_TYPE_num(seq) == 6) {
264		t = sk_ASN1_TYPE_value(seq, i++);
265		if (t->type != V_ASN1_INTEGER) {
266			warnx("%s: RFC 6486 section 4.2.1: version: "
267			    "want ASN.1 integer, have %s (NID %d)",
268			    p->fn, ASN1_tag2str(t->type), t->type);
269			goto out;
270		}
271	}
272
273	/* Now the manifest sequence number. */
274
275	t = sk_ASN1_TYPE_value(seq, i++);
276	if (t->type != V_ASN1_INTEGER) {
277		warnx("%s: RFC 6486 section 4.2.1: manifestNumber: "
278		    "want ASN.1 integer, have %s (NID %d)",
279		    p->fn, ASN1_tag2str(t->type), t->type);
280		goto out;
281	}
282
283	/*
284	 * Timestamps: this and next update time.
285	 * Validate that the current date falls into this interval.
286	 * This is required by section 4.4, (3).
287	 * If we're after the given date, then the MFT is stale.
288	 * This is made super complicated because it usees OpenSSL's
289	 * ASN1_GENERALIZEDTIME instead of ASN1_TIME, which we could
290	 * compare against the current time trivially.
291	 */
292
293	t = sk_ASN1_TYPE_value(seq, i++);
294	if (t->type != V_ASN1_GENERALIZEDTIME) {
295		warnx("%s: RFC 6486 section 4.2.1: thisUpdate: "
296		    "want ASN.1 generalised time, have %s (NID %d)",
297		    p->fn, ASN1_tag2str(t->type), t->type);
298		goto out;
299	}
300	from = t->value.generalizedtime;
301
302	t = sk_ASN1_TYPE_value(seq, i++);
303	if (t->type != V_ASN1_GENERALIZEDTIME) {
304		warnx("%s: RFC 6486 section 4.2.1: nextUpdate: "
305		    "want ASN.1 generalised time, have %s (NID %d)",
306		    p->fn, ASN1_tag2str(t->type), t->type);
307		goto out;
308	}
309	until = t->value.generalizedtime;
310
311	rc = check_validity(from, until, p->fn, force);
312	if (rc != 1)
313		goto out;
314
315	/* File list algorithm. */
316
317	t = sk_ASN1_TYPE_value(seq, i++);
318	if (t->type != V_ASN1_OBJECT) {
319		warnx("%s: RFC 6486 section 4.2.1: fileHashAlg: "
320		    "want ASN.1 object time, have %s (NID %d)",
321		    p->fn, ASN1_tag2str(t->type), t->type);
322		goto out;
323	} else if (OBJ_obj2nid(t->value.object) != NID_sha256) {
324		warnx("%s: RFC 6486 section 4.2.1: fileHashAlg: "
325		    "want SHA256 object, have %s (NID %d)", p->fn,
326		    ASN1_tag2str(OBJ_obj2nid(t->value.object)),
327		    OBJ_obj2nid(t->value.object));
328		goto out;
329	}
330
331	/* Now the sequence. */
332
333	t = sk_ASN1_TYPE_value(seq, i++);
334	if (t->type != V_ASN1_SEQUENCE) {
335		warnx("%s: RFC 6486 section 4.2.1: fileList: "
336		    "want ASN.1 sequence, have %s (NID %d)",
337		    p->fn, ASN1_tag2str(t->type), t->type);
338		goto out;
339	} else if (!mft_parse_flist(p, t->value.octet_string))
340		goto out;
341
342	rc = 1;
343out:
344	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
345	return rc;
346}
347
348/*
349 * Parse the objects that have been published in the manifest.
350 * This conforms to RFC 6486.
351 * Note that if the MFT is stale, all referenced objects are stripped
352 * from the parsed content.
353 * The MFT content is otherwise returned.
354 */
355struct mft *
356mft_parse(X509 **x509, const char *fn, int force)
357{
358	struct parse	 p;
359	int		 c, rc = 0;
360	size_t		 i, cmsz;
361	unsigned char	*cms;
362
363	memset(&p, 0, sizeof(struct parse));
364	p.fn = fn;
365
366	cms = cms_parse_validate(x509, fn, "1.2.840.113549.1.9.16.1.26",
367	    NULL, &cmsz);
368	if (cms == NULL)
369		return NULL;
370	assert(*x509 != NULL);
371
372	if ((p.res = calloc(1, sizeof(struct mft))) == NULL)
373		err(EXIT_FAILURE, NULL);
374	if ((p.res->file = strdup(fn)) == NULL)
375		err(EXIT_FAILURE, NULL);
376	if (!x509_get_ski_aki(*x509, fn, &p.res->ski, &p.res->aki))
377		goto out;
378
379	/*
380	 * If we're stale, then remove all of the files that the MFT
381	 * references as well as marking it as stale.
382	 */
383
384	if ((c = mft_parse_econtent(cms, cmsz, &p, force)) == 0) {
385		/*
386		 * FIXME: it should suffice to just mark this as stale
387		 * and have the logic around mft_read() simply ignore
388		 * the contents of stale entries, just like it does for
389		 * invalid ROAs or certificates.
390		 */
391
392		p.res->stale = 1;
393		if (p.res->files != NULL)
394			for (i = 0; i < p.res->filesz; i++)
395				free(p.res->files[i].file);
396		free(p.res->files);
397		p.res->filesz = 0;
398		p.res->files = NULL;
399	} else if (c == -1)
400		goto out;
401
402	rc = 1;
403out:
404	if (rc == 0) {
405		mft_free(p.res);
406		p.res = NULL;
407		X509_free(*x509);
408		*x509 = NULL;
409	}
410	free(cms);
411	return p.res;
412}
413
414/*
415 * Free an MFT pointer.
416 * Safe to call with NULL.
417 */
418void
419mft_free(struct mft *p)
420{
421	size_t	 i;
422
423	if (p == NULL)
424		return;
425
426	if (p->files != NULL)
427		for (i = 0; i < p->filesz; i++)
428			free(p->files[i].file);
429
430	free(p->aki);
431	free(p->ski);
432	free(p->file);
433	free(p->files);
434	free(p);
435}
436
437/*
438 * Serialise MFT parsed content into the given buffer.
439 * See mft_read() for the other side of the pipe.
440 */
441void
442mft_buffer(char **b, size_t *bsz, size_t *bmax, const struct mft *p)
443{
444	size_t		 i;
445
446	io_simple_buffer(b, bsz, bmax, &p->stale, sizeof(int));
447	io_str_buffer(b, bsz, bmax, p->file);
448	io_simple_buffer(b, bsz, bmax, &p->filesz, sizeof(size_t));
449
450	for (i = 0; i < p->filesz; i++) {
451		io_str_buffer(b, bsz, bmax, p->files[i].file);
452		io_simple_buffer(b, bsz, bmax,
453			p->files[i].hash, SHA256_DIGEST_LENGTH);
454	}
455
456	io_str_buffer(b, bsz, bmax, p->aki);
457	io_str_buffer(b, bsz, bmax, p->ski);
458}
459
460/*
461 * Read an MFT structure from the file descriptor.
462 * Result must be passed to mft_free().
463 */
464struct mft *
465mft_read(int fd)
466{
467	struct mft	*p = NULL;
468	size_t		 i;
469
470	if ((p = calloc(1, sizeof(struct mft))) == NULL)
471		err(EXIT_FAILURE, NULL);
472
473	io_simple_read(fd, &p->stale, sizeof(int));
474	io_str_read(fd, &p->file);
475	io_simple_read(fd, &p->filesz, sizeof(size_t));
476
477	if ((p->files = calloc(p->filesz, sizeof(struct mftfile))) == NULL)
478		err(EXIT_FAILURE, NULL);
479
480	for (i = 0; i < p->filesz; i++) {
481		io_str_read(fd, &p->files[i].file);
482		io_simple_read(fd, p->files[i].hash, SHA256_DIGEST_LENGTH);
483	}
484
485	io_str_read(fd, &p->aki);
486	io_str_read(fd, &p->ski);
487	return p;
488}
489