mft.c revision 1.63
1/*	$OpenBSD: mft.c,v 1.63 2022/05/10 07:41:37 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 <ctype.h>
20#include <err.h>
21#include <limits.h>
22#include <stdarg.h>
23#include <stdint.h>
24#include <stdlib.h>
25#include <string.h>
26#include <unistd.h>
27
28#include <openssl/bn.h>
29#include <openssl/asn1.h>
30#include <openssl/sha.h>
31#include <openssl/x509.h>
32
33#include "extern.h"
34
35/*
36 * Parse results and data of the manifest file.
37 */
38struct	parse {
39	const char	*fn; /* manifest file name */
40	struct mft	*res; /* result object */
41	int		 found_crl;
42};
43
44extern ASN1_OBJECT    *mft_oid;
45
46/*
47 * Convert an ASN1_GENERALIZEDTIME to a struct tm.
48 * Returns 1 on success, 0 on failure.
49 */
50static int
51generalizedtime_to_tm(const ASN1_GENERALIZEDTIME *gtime, struct tm *tm)
52{
53	const char *data;
54	size_t len;
55
56	data = ASN1_STRING_get0_data(gtime);
57	len = ASN1_STRING_length(gtime);
58
59	memset(tm, 0, sizeof(*tm));
60	return ASN1_time_parse(data, len, tm, V_ASN1_GENERALIZEDTIME) ==
61	    V_ASN1_GENERALIZEDTIME;
62}
63
64/*
65 * Validate and verify the time validity of the mft.
66 * Returns 1 if all is good and for any other case 0.
67 */
68static int
69mft_parse_time(const ASN1_GENERALIZEDTIME *from,
70    const ASN1_GENERALIZEDTIME *until, struct parse *p)
71{
72	struct tm tm_from, tm_until;
73
74	if (!generalizedtime_to_tm(from, &tm_from)) {
75		warnx("%s: embedded from time format invalid", p->fn);
76		return 0;
77	}
78	if (!generalizedtime_to_tm(until, &tm_until)) {
79		warnx("%s: embedded until time format invalid", p->fn);
80		return 0;
81	}
82
83	/* check that until is not before from */
84	if (ASN1_time_tm_cmp(&tm_until, &tm_from) < 0) {
85		warnx("%s: bad update interval", p->fn);
86		return 0;
87	}
88
89	if ((p->res->valid_since = timegm(&tm_from)) == -1 ||
90	    (p->res->valid_until = timegm(&tm_until)) == -1)
91		errx(1, "%s: timegm failed", p->fn);
92
93	return 1;
94}
95
96/*
97 * Determine rtype corresponding to file extension. Returns RTYPE_INVALID
98 * on error or unkown extension.
99 */
100enum rtype
101rtype_from_file_extension(const char *fn)
102{
103	size_t	 sz;
104
105	sz = strlen(fn);
106	if (sz < 5)
107		return RTYPE_INVALID;
108
109	if (strcasecmp(fn + sz - 4, ".tal") == 0)
110		return RTYPE_TAL;
111	if (strcasecmp(fn + sz - 4, ".cer") == 0)
112		return RTYPE_CER;
113	if (strcasecmp(fn + sz - 4, ".crl") == 0)
114		return RTYPE_CRL;
115	if (strcasecmp(fn + sz - 4, ".mft") == 0)
116		return RTYPE_MFT;
117	if (strcasecmp(fn + sz - 4, ".roa") == 0)
118		return RTYPE_ROA;
119	if (strcasecmp(fn + sz - 4, ".gbr") == 0)
120		return RTYPE_GBR;
121	if (strcasecmp(fn + sz - 4, ".asa") == 0)
122		return RTYPE_ASPA;
123	if (strcasecmp(fn + sz - 4, ".sig") == 0)
124		return RTYPE_RSC;
125
126	return RTYPE_INVALID;
127}
128
129/*
130 * Validate that a filename listed on a Manifest only contains characters
131 * permitted in draft-ietf-sidrops-6486bis section 4.2.2
132 * Also ensure that there is exactly one '.'.
133 */
134static int
135valid_mft_filename(const char *fn, size_t len)
136{
137	const unsigned char *c;
138
139	if (!valid_filename(fn, len))
140		return 0;
141
142	c = memchr(fn, '.', len);
143	if (c == NULL || c != memrchr(fn, '.', len))
144		return 0;
145
146	return 1;
147}
148
149/*
150 * Check that the file is an ASPA, CER, CRL, GBR or a ROA.
151 * Returns corresponding rtype or RTYPE_INVALID on error.
152 */
153static enum rtype
154rtype_from_mftfile(const char *fn)
155{
156	enum rtype		 type;
157
158	type = rtype_from_file_extension(fn);
159	switch (type) {
160	case RTYPE_ASPA:
161	case RTYPE_CER:
162	case RTYPE_CRL:
163	case RTYPE_GBR:
164	case RTYPE_ROA:
165		return type;
166	default:
167		return RTYPE_INVALID;
168	}
169}
170
171/*
172 * Parse an individual "FileAndHash", RFC 6486, sec. 4.2.
173 * Return zero on failure, non-zero on success.
174 */
175static int
176mft_parse_filehash(struct parse *p, const ASN1_OCTET_STRING *os)
177{
178	ASN1_SEQUENCE_ANY	*seq;
179	const ASN1_TYPE		*file, *hash;
180	char			*fn = NULL;
181	const unsigned char	*d = os->data;
182	size_t			 dsz = os->length;
183	int			 rc = 0;
184	struct mftfile		*fent;
185	enum rtype		 type;
186
187	if ((seq = d2i_ASN1_SEQUENCE_ANY(NULL, &d, dsz)) == NULL) {
188		cryptowarnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
189		    "failed ASN.1 sequence parse", p->fn);
190		goto out;
191	}
192	if (sk_ASN1_TYPE_num(seq) != 2) {
193		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
194		    "want 2 elements, have %d", p->fn,
195		    sk_ASN1_TYPE_num(seq));
196		goto out;
197	}
198
199	/* First is the filename itself. */
200
201	file = sk_ASN1_TYPE_value(seq, 0);
202	if (file->type != V_ASN1_IA5STRING) {
203		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
204		    "want ASN.1 IA5 string, have %s (NID %d)",
205		    p->fn, ASN1_tag2str(file->type), file->type);
206		goto out;
207	}
208	if (!valid_mft_filename(file->value.ia5string->data,
209	    file->value.ia5string->length)) {
210		warnx("%s: RFC 6486 section 4.2.2: bad filename", p->fn);
211		goto out;
212	}
213	fn = strndup((const char *)file->value.ia5string->data,
214	    file->value.ia5string->length);
215	if (fn == NULL)
216		err(1, NULL);
217
218	/* Now hash value. */
219
220	hash = sk_ASN1_TYPE_value(seq, 1);
221	if (hash->type != V_ASN1_BIT_STRING) {
222		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
223		    "want ASN.1 bit string, have %s (NID %d)",
224		    p->fn, ASN1_tag2str(hash->type), hash->type);
225		goto out;
226	}
227
228	if (hash->value.bit_string->length != SHA256_DIGEST_LENGTH) {
229		warnx("%s: RFC 6486 section 4.2.1: hash: "
230		    "invalid SHA256 length, have %d",
231		    p->fn, hash->value.bit_string->length);
232		goto out;
233	}
234
235	type = rtype_from_mftfile(fn);
236	/* remember the filehash for the CRL in struct mft */
237	if (type == RTYPE_CRL && strcmp(fn, p->res->crl) == 0) {
238		memcpy(p->res->crlhash, hash->value.bit_string->data,
239		    SHA256_DIGEST_LENGTH);
240		p->found_crl = 1;
241	}
242
243	/* Insert the filename and hash value. */
244	fent = &p->res->files[p->res->filesz++];
245	fent->type = type;
246	fent->file = fn;
247	fn = NULL;
248	memcpy(fent->hash, hash->value.bit_string->data, SHA256_DIGEST_LENGTH);
249
250	rc = 1;
251out:
252	free(fn);
253	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
254	return rc;
255}
256
257/*
258 * Parse the "FileAndHash" sequence, RFC 6486, sec. 4.2.
259 * Return zero on failure, non-zero on success.
260 */
261static int
262mft_parse_flist(struct parse *p, const ASN1_OCTET_STRING *os)
263{
264	ASN1_SEQUENCE_ANY	*seq;
265	const ASN1_TYPE		*t;
266	const unsigned char	*d = os->data;
267	size_t			 dsz = os->length;
268	int			 i, rc = 0;
269
270	if ((seq = d2i_ASN1_SEQUENCE_ANY(NULL, &d, dsz)) == NULL) {
271		cryptowarnx("%s: RFC 6486 section 4.2: fileList: "
272		    "failed ASN.1 sequence parse", p->fn);
273		goto out;
274	}
275
276	if (sk_ASN1_TYPE_num(seq) > MAX_MANIFEST_ENTRIES) {
277		warnx("%s: %d exceeds manifest entry limit (%d)", p->fn,
278		    sk_ASN1_TYPE_num(seq), MAX_MANIFEST_ENTRIES);
279		goto out;
280	}
281
282	p->res->files = calloc(sk_ASN1_TYPE_num(seq), sizeof(struct mftfile));
283	if (p->res->files == NULL)
284		err(1, NULL);
285
286	for (i = 0; i < sk_ASN1_TYPE_num(seq); i++) {
287		t = sk_ASN1_TYPE_value(seq, i);
288		if (t->type != V_ASN1_SEQUENCE) {
289			warnx("%s: RFC 6486 section 4.2: fileList: "
290			    "want ASN.1 sequence, have %s (NID %d)",
291			    p->fn, ASN1_tag2str(t->type), t->type);
292			goto out;
293		}
294		if (!mft_parse_filehash(p, t->value.octet_string))
295			goto out;
296	}
297
298	if (!p->found_crl) {
299		warnx("%s: CRL not part of MFT fileList", p->fn);
300		goto out;
301	}
302
303	rc = 1;
304 out:
305	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
306	return rc;
307}
308
309/*
310 * Handle the eContent of the manifest object, RFC 6486 sec. 4.2.
311 * Returns 0 on failure and 1 on success.
312 */
313static int
314mft_parse_econtent(const unsigned char *d, size_t dsz, struct parse *p)
315{
316	ASN1_SEQUENCE_ANY	*seq;
317	const ASN1_TYPE		*t;
318	const ASN1_GENERALIZEDTIME *from, *until;
319	long			 mft_version;
320	int			 i = 0, rc = 0;
321
322	if ((seq = d2i_ASN1_SEQUENCE_ANY(NULL, &d, dsz)) == NULL) {
323		cryptowarnx("%s: RFC 6486 section 4.2: Manifest: "
324		    "failed ASN.1 sequence parse", p->fn);
325		goto out;
326	}
327
328	/* Test if the optional profile version field is present. */
329	if (sk_ASN1_TYPE_num(seq) != 5 &&
330	    sk_ASN1_TYPE_num(seq) != 6) {
331		warnx("%s: RFC 6486 section 4.2: Manifest: "
332		    "want 5 or 6 elements, have %d", p->fn,
333		    sk_ASN1_TYPE_num(seq));
334		goto out;
335	}
336
337	/* Parse the optional version field */
338	if (sk_ASN1_TYPE_num(seq) == 6) {
339		t = sk_ASN1_TYPE_value(seq, i++);
340		d = t->value.asn1_string->data;
341		dsz = t->value.asn1_string->length;
342
343		if (cms_econtent_version(p->fn, &d, dsz, &mft_version) == -1)
344			goto out;
345
346		switch (mft_version) {
347		case 0:
348			warnx("%s: incorrect encoding for version 0", p->fn);
349			goto out;
350		default:
351			warnx("%s: version %ld not supported (yet)", p->fn,
352			    mft_version);
353			goto out;
354		}
355	}
356
357	/* Now the manifest sequence number. */
358
359	t = sk_ASN1_TYPE_value(seq, i++);
360	if (t->type != V_ASN1_INTEGER) {
361		warnx("%s: RFC 6486 section 4.2.1: manifestNumber: "
362		    "want ASN.1 integer, have %s (NID %d)",
363		    p->fn, ASN1_tag2str(t->type), t->type);
364		goto out;
365	}
366
367	p->res->seqnum = x509_convert_seqnum(p->fn, t->value.integer);
368	if (p->res->seqnum == NULL)
369		goto out;
370
371	/*
372	 * Timestamps: this and next update time.
373	 * Validate that the current date falls into this interval.
374	 * This is required by section 4.4, (3).
375	 * If we're after the given date, then the MFT is stale.
376	 * This is made super complicated because it uses OpenSSL's
377	 * ASN1_GENERALIZEDTIME instead of ASN1_TIME, which we could
378	 * compare against the current time trivially.
379	 */
380
381	t = sk_ASN1_TYPE_value(seq, i++);
382	if (t->type != V_ASN1_GENERALIZEDTIME) {
383		warnx("%s: RFC 6486 section 4.2.1: thisUpdate: "
384		    "want ASN.1 generalised time, have %s (NID %d)",
385		    p->fn, ASN1_tag2str(t->type), t->type);
386		goto out;
387	}
388	from = t->value.generalizedtime;
389
390	t = sk_ASN1_TYPE_value(seq, i++);
391	if (t->type != V_ASN1_GENERALIZEDTIME) {
392		warnx("%s: RFC 6486 section 4.2.1: nextUpdate: "
393		    "want ASN.1 generalised time, have %s (NID %d)",
394		    p->fn, ASN1_tag2str(t->type), t->type);
395		goto out;
396	}
397	until = t->value.generalizedtime;
398
399	if (!mft_parse_time(from, until, p))
400		goto out;
401
402	/* File list algorithm. */
403
404	t = sk_ASN1_TYPE_value(seq, i++);
405	if (t->type != V_ASN1_OBJECT) {
406		warnx("%s: RFC 6486 section 4.2.1: fileHashAlg: "
407		    "want ASN.1 object, have %s (NID %d)",
408		    p->fn, ASN1_tag2str(t->type), t->type);
409		goto out;
410	}
411	if (OBJ_obj2nid(t->value.object) != NID_sha256) {
412		warnx("%s: RFC 6486 section 4.2.1: fileHashAlg: "
413		    "want SHA256 object, have %s (NID %d)", p->fn,
414		    ASN1_tag2str(OBJ_obj2nid(t->value.object)),
415		    OBJ_obj2nid(t->value.object));
416		goto out;
417	}
418
419	/* Now the sequence. */
420
421	t = sk_ASN1_TYPE_value(seq, i++);
422	if (t->type != V_ASN1_SEQUENCE) {
423		warnx("%s: RFC 6486 section 4.2.1: fileList: "
424		    "want ASN.1 sequence, have %s (NID %d)",
425		    p->fn, ASN1_tag2str(t->type), t->type);
426		goto out;
427	}
428
429	if (!mft_parse_flist(p, t->value.octet_string))
430		goto out;
431
432	rc = 1;
433out:
434	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
435	return rc;
436}
437
438/*
439 * Parse the objects that have been published in the manifest.
440 * This conforms to RFC 6486.
441 * Note that if the MFT is stale, all referenced objects are stripped
442 * from the parsed content.
443 * The MFT content is otherwise returned.
444 */
445struct mft *
446mft_parse(X509 **x509, const char *fn, const unsigned char *der, size_t len)
447{
448	struct parse	 p;
449	int		 rc = 0;
450	size_t		 cmsz;
451	unsigned char	*cms;
452	char		*crldp = NULL, *crlfile;
453
454	memset(&p, 0, sizeof(struct parse));
455	p.fn = fn;
456
457	cms = cms_parse_validate(x509, fn, der, len, mft_oid, &cmsz);
458	if (cms == NULL)
459		return NULL;
460	assert(*x509 != NULL);
461
462	if ((p.res = calloc(1, sizeof(struct mft))) == NULL)
463		err(1, NULL);
464
465	if (!x509_get_aia(*x509, fn, &p.res->aia))
466		goto out;
467	if (!x509_get_aki(*x509, fn, &p.res->aki))
468		goto out;
469	if (!x509_get_ski(*x509, fn, &p.res->ski))
470		goto out;
471	if (p.res->aia == NULL || p.res->aki == NULL || p.res->ski == NULL) {
472		warnx("%s: RFC 6487 section 4.8: "
473		    "missing AIA, AKI or SKI X509 extension", fn);
474		goto out;
475	}
476
477	/* get CRL info for later */
478	if (!x509_get_crl(*x509, fn, &crldp))
479		goto out;
480	if (crldp == NULL) {
481		warnx("%s: RFC 6487 section 4.8.6: CRL: "
482		    "missing CRL distribution point extension", fn);
483		goto out;
484	}
485	if ((crlfile = strrchr(crldp, '/')) == NULL ||
486	    !valid_mft_filename(crlfile + 1, strlen(crlfile + 1)) ||
487	    rtype_from_file_extension(crlfile + 1) != RTYPE_CRL) {
488		warnx("%s: RFC 6487 section 4.8.6: CRL: "
489		    "bad CRL distribution point extension", fn);
490		goto out;
491	}
492	if ((p.res->crl = strdup(crlfile + 1)) == NULL)
493		err(1, NULL);
494
495	if (mft_parse_econtent(cms, cmsz, &p) == 0)
496		goto out;
497
498	rc = 1;
499out:
500	if (rc == 0) {
501		mft_free(p.res);
502		p.res = NULL;
503		X509_free(*x509);
504		*x509 = NULL;
505	}
506	free(crldp);
507	free(cms);
508	return p.res;
509}
510
511/*
512 * Free an MFT pointer.
513 * Safe to call with NULL.
514 */
515void
516mft_free(struct mft *p)
517{
518	size_t	 i;
519
520	if (p == NULL)
521		return;
522
523	if (p->files != NULL)
524		for (i = 0; i < p->filesz; i++)
525			free(p->files[i].file);
526
527	free(p->aia);
528	free(p->aki);
529	free(p->ski);
530	free(p->path);
531	free(p->files);
532	free(p->seqnum);
533	free(p);
534}
535
536/*
537 * Serialise MFT parsed content into the given buffer.
538 * See mft_read() for the other side of the pipe.
539 */
540void
541mft_buffer(struct ibuf *b, const struct mft *p)
542{
543	size_t		 i;
544
545	io_simple_buffer(b, &p->stale, sizeof(p->stale));
546	io_simple_buffer(b, &p->repoid, sizeof(p->repoid));
547	io_str_buffer(b, p->path);
548
549	io_str_buffer(b, p->aia);
550	io_str_buffer(b, p->aki);
551	io_str_buffer(b, p->ski);
552
553	io_simple_buffer(b, &p->filesz, sizeof(size_t));
554	for (i = 0; i < p->filesz; i++) {
555		io_str_buffer(b, p->files[i].file);
556		io_simple_buffer(b, &p->files[i].type,
557		    sizeof(p->files[i].type));
558		io_simple_buffer(b, &p->files[i].location,
559		    sizeof(p->files[i].location));
560		io_simple_buffer(b, p->files[i].hash, SHA256_DIGEST_LENGTH);
561	}
562}
563
564/*
565 * Read an MFT structure from the file descriptor.
566 * Result must be passed to mft_free().
567 */
568struct mft *
569mft_read(struct ibuf *b)
570{
571	struct mft	*p = NULL;
572	size_t		 i;
573
574	if ((p = calloc(1, sizeof(struct mft))) == NULL)
575		err(1, NULL);
576
577	io_read_buf(b, &p->stale, sizeof(p->stale));
578	io_read_buf(b, &p->repoid, sizeof(p->repoid));
579	io_read_str(b, &p->path);
580
581	io_read_str(b, &p->aia);
582	io_read_str(b, &p->aki);
583	io_read_str(b, &p->ski);
584	assert(p->aia && p->aki && p->ski);
585
586	io_read_buf(b, &p->filesz, sizeof(size_t));
587	if ((p->files = calloc(p->filesz, sizeof(struct mftfile))) == NULL)
588		err(1, NULL);
589
590	for (i = 0; i < p->filesz; i++) {
591		io_read_str(b, &p->files[i].file);
592		io_read_buf(b, &p->files[i].type, sizeof(p->files[i].type));
593		io_read_buf(b, &p->files[i].location,
594		    sizeof(p->files[i].location));
595		io_read_buf(b, p->files[i].hash, SHA256_DIGEST_LENGTH);
596	}
597
598	return p;
599}
600
601/*
602 * Compare two MFT files, returns 1 if first MFT is preferred and 0 if second
603 * MFT should be used.
604 */
605int
606mft_compare(const struct mft *a, const struct mft *b)
607{
608	int r;
609
610	if (b == NULL)
611		return 1;
612	if (a == NULL)
613		return 0;
614
615	r = strlen(a->seqnum) - strlen(b->seqnum);
616	if (r > 0)	/* seqnum in a is longer -> higher */
617		return 1;
618	if (r < 0)	/* seqnum in a is shorter -> smaller */
619		return 0;
620
621	r = strcmp(a->seqnum, b->seqnum);
622	if (r >= 0)	/* a is greater or equal, prefer a */
623		return 1;
624	return 0;
625}
626