mft.c revision 1.50
1/*	$OpenBSD: mft.c,v 1.50 2022/01/22 09:18:48 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};
42
43extern ASN1_OBJECT    *mft_oid;
44
45static const char *
46gentime2str(const ASN1_GENERALIZEDTIME *time)
47{
48	static char	buf[64];
49	BIO		*mem;
50
51	if ((mem = BIO_new(BIO_s_mem())) == NULL)
52		cryptoerrx("BIO_new");
53	if (!ASN1_GENERALIZEDTIME_print(mem, time))
54		cryptoerrx("ASN1_GENERALIZEDTIME_print");
55	if (BIO_gets(mem, buf, sizeof(buf)) < 0)
56		cryptoerrx("BIO_gets");
57
58	BIO_free(mem);
59	return buf;
60}
61
62/*
63 * Convert an ASN1_GENERALIZEDTIME to a struct tm.
64 * Returns 1 on success, 0 on failure.
65 */
66static int
67generalizedtime_to_tm(const ASN1_GENERALIZEDTIME *gtime, struct tm *tm)
68{
69	const char *data;
70	size_t len;
71
72	data = ASN1_STRING_get0_data(gtime);
73	len = ASN1_STRING_length(gtime);
74
75	memset(tm, 0, sizeof(*tm));
76	return ASN1_time_parse(data, len, tm, V_ASN1_GENERALIZEDTIME) ==
77	    V_ASN1_GENERALIZEDTIME;
78}
79
80/*
81 * Validate and verify the time validity of the mft.
82 * Returns 1 if all is good, 0 if mft is stale, any other case -1.
83 */
84static int
85check_validity(const ASN1_GENERALIZEDTIME *from,
86    const ASN1_GENERALIZEDTIME *until, const char *fn)
87{
88	time_t now = time(NULL);
89	struct tm tm_from, tm_until, tm_now;
90
91	if (gmtime_r(&now, &tm_now) == NULL) {
92		warnx("%s: could not get current time", fn);
93		return -1;
94	}
95
96	if (!generalizedtime_to_tm(from, &tm_from)) {
97		warnx("%s: embedded from time format invalid", fn);
98		return -1;
99	}
100	if (!generalizedtime_to_tm(until, &tm_until)) {
101		warnx("%s: embedded until time format invalid", fn);
102		return -1;
103	}
104
105	/* check that until is not before from */
106	if (ASN1_time_tm_cmp(&tm_until, &tm_from) < 0) {
107		warnx("%s: bad update interval", fn);
108		return -1;
109	}
110	/* check that now is not before from */
111	if (ASN1_time_tm_cmp(&tm_from, &tm_now) > 0) {
112		warnx("%s: mft not yet valid %s", fn, gentime2str(from));
113		return -1;
114	}
115	/* check that now is not after until */
116	if (ASN1_time_tm_cmp(&tm_until, &tm_now) < 0) {
117		warnx("%s: mft expired on %s", fn, gentime2str(until));
118		return 0;
119	}
120
121	return 1;
122}
123
124/*
125 * Determine rtype corresponding to file extension. Returns RTYPE_INVALID
126 * on error or unkown extension.
127 */
128enum rtype
129rtype_from_file_extension(const char *fn)
130{
131	size_t	 sz;
132
133	sz = strlen(fn);
134	if (sz < 5)
135		return RTYPE_INVALID;
136
137	if (strcasecmp(fn + sz - 4, ".tal") == 0)
138		return RTYPE_TAL;
139	if (strcasecmp(fn + sz - 4, ".cer") == 0)
140		return RTYPE_CER;
141	if (strcasecmp(fn + sz - 4, ".crl") == 0)
142		return RTYPE_CRL;
143	if (strcasecmp(fn + sz - 4, ".mft") == 0)
144		return RTYPE_MFT;
145	if (strcasecmp(fn + sz - 4, ".roa") == 0)
146		return RTYPE_ROA;
147	if (strcasecmp(fn + sz - 4, ".gbr") == 0)
148		return RTYPE_GBR;
149
150	return RTYPE_INVALID;
151}
152
153/*
154 * Validate that a filename listed on a Manifest only contains characters
155 * permitted in draft-ietf-sidrops-6486bis section 4.2.2 and check that
156 * it's a CER, CRL, GBR or a ROA.
157 * Returns corresponding rtype or RTYPE_INVALID on error.
158 */
159enum rtype
160rtype_from_mftfile(const char *fn)
161{
162	const unsigned char	*c;
163	enum rtype		 type;
164
165	for (c = fn; *c != '\0'; ++c)
166		if (!isalnum(*c) && *c != '-' && *c != '_' && *c != '.')
167			return RTYPE_INVALID;
168
169	if (strchr(fn, '.') != strrchr(fn, '.'))
170		return RTYPE_INVALID;
171
172	type = rtype_from_file_extension(fn);
173	switch (type) {
174	case RTYPE_CER:
175	case RTYPE_CRL:
176	case RTYPE_GBR:
177	case RTYPE_ROA:
178		return type;
179	default:
180		return RTYPE_INVALID;
181	}
182}
183
184/*
185 * Parse an individual "FileAndHash", RFC 6486, sec. 4.2.
186 * Return zero on failure, non-zero on success.
187 */
188static int
189mft_parse_filehash(struct parse *p, const ASN1_OCTET_STRING *os)
190{
191	ASN1_SEQUENCE_ANY	*seq;
192	const ASN1_TYPE		*file, *hash;
193	char			*fn = NULL;
194	enum rtype		 type;
195	const unsigned char	*d = os->data;
196	size_t			 dsz = os->length;
197	int			 rc = 0;
198	struct mftfile		*fent;
199
200	if ((seq = d2i_ASN1_SEQUENCE_ANY(NULL, &d, dsz)) == NULL) {
201		cryptowarnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
202		    "failed ASN.1 sequence parse", p->fn);
203		goto out;
204	} else if (sk_ASN1_TYPE_num(seq) != 2) {
205		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
206		    "want 2 elements, have %d", p->fn,
207		    sk_ASN1_TYPE_num(seq));
208		goto out;
209	}
210
211	/* First is the filename itself. */
212
213	file = sk_ASN1_TYPE_value(seq, 0);
214	if (file->type != V_ASN1_IA5STRING) {
215		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
216		    "want ASN.1 IA5 string, have %s (NID %d)",
217		    p->fn, ASN1_tag2str(file->type), file->type);
218		goto out;
219	}
220	fn = strndup((const char *)file->value.ia5string->data,
221	    file->value.ia5string->length);
222	if (fn == NULL)
223		err(1, NULL);
224
225	if ((type = rtype_from_mftfile(fn)) == RTYPE_INVALID) {
226		warnx("%s: invalid filename: %s", p->fn, fn);
227		goto out;
228	}
229
230	/* Now hash value. */
231
232	hash = sk_ASN1_TYPE_value(seq, 1);
233	if (hash->type != V_ASN1_BIT_STRING) {
234		warnx("%s: RFC 6486 section 4.2.1: FileAndHash: "
235		    "want ASN.1 bit string, have %s (NID %d)",
236		    p->fn, ASN1_tag2str(hash->type), hash->type);
237		goto out;
238	}
239
240	if (hash->value.bit_string->length != SHA256_DIGEST_LENGTH) {
241		warnx("%s: RFC 6486 section 4.2.1: hash: "
242		    "invalid SHA256 length, have %d",
243		    p->fn, hash->value.bit_string->length);
244		goto out;
245	}
246
247	/* Insert the filename and hash value. */
248	fent = &p->res->files[p->res->filesz++];
249
250	fent->file = fn;
251	fent->type = type;
252	fn = NULL;
253	memcpy(fent->hash, hash->value.bit_string->data, SHA256_DIGEST_LENGTH);
254
255	rc = 1;
256out:
257	free(fn);
258	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
259	return rc;
260}
261
262/*
263 * Parse the "FileAndHash" sequence, RFC 6486, sec. 4.2.
264 * Return zero on failure, non-zero on success.
265 */
266static int
267mft_parse_flist(struct parse *p, const ASN1_OCTET_STRING *os)
268{
269	ASN1_SEQUENCE_ANY	*seq;
270	const ASN1_TYPE		*t;
271	const unsigned char	*d = os->data;
272	size_t			 dsz = os->length;
273	int			 i, rc = 0;
274
275	if ((seq = d2i_ASN1_SEQUENCE_ANY(NULL, &d, dsz)) == NULL) {
276		cryptowarnx("%s: RFC 6486 section 4.2: fileList: "
277		    "failed ASN.1 sequence parse", p->fn);
278		goto out;
279	}
280
281	if (sk_ASN1_TYPE_num(seq) > MAX_MANIFEST_ENTRIES) {
282		warnx("%s: %d exceeds manifest entry limit (%d)", p->fn,
283		    sk_ASN1_TYPE_num(seq), MAX_MANIFEST_ENTRIES);
284		goto out;
285	}
286
287	p->res->files = calloc(sk_ASN1_TYPE_num(seq), sizeof(struct mftfile));
288	if (p->res->files == NULL)
289		err(1, NULL);
290
291	for (i = 0; i < sk_ASN1_TYPE_num(seq); i++) {
292		t = sk_ASN1_TYPE_value(seq, i);
293		if (t->type != V_ASN1_SEQUENCE) {
294			warnx("%s: RFC 6486 section 4.2: fileList: "
295			    "want ASN.1 sequence, have %s (NID %d)",
296			    p->fn, ASN1_tag2str(t->type), t->type);
297			goto out;
298		} else if (!mft_parse_filehash(p, t->value.octet_string))
299			goto out;
300	}
301
302	rc = 1;
303 out:
304	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
305	return rc;
306}
307
308/*
309 * Handle the eContent of the manifest object, RFC 6486 sec. 4.2.
310 * Returns 0 on failure and 1 on success.
311 */
312static int
313mft_parse_econtent(const unsigned char *d, size_t dsz, struct parse *p)
314{
315	ASN1_SEQUENCE_ANY	*seq;
316	const ASN1_TYPE		*t;
317	const ASN1_GENERALIZEDTIME *from, *until;
318	long			 mft_version;
319	BIGNUM			*mft_seqnum = NULL;
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	mft_seqnum = ASN1_INTEGER_to_BN(t->value.integer, NULL);
368	if (mft_seqnum == NULL) {
369		warnx("%s: ASN1_INTEGER_to_BN error", p->fn);
370		goto out;
371	}
372
373	if (BN_is_negative(mft_seqnum)) {
374		warnx("%s: RFC 6486 section 4.2.1: manifestNumber: "
375		    "want positive integer, have negative.", p->fn);
376		goto out;
377	}
378
379	if (BN_num_bytes(mft_seqnum) > 20) {
380		warnx("%s: RFC 6486 section 4.2.1: manifestNumber: "
381		    "want 20 or less than octets, have more.", p->fn);
382		goto out;
383	}
384
385	p->res->seqnum = BN_bn2hex(mft_seqnum);
386	if (p->res->seqnum == NULL) {
387		warnx("%s: BN_bn2hex error", p->fn);
388		goto out;
389	}
390
391	/*
392	 * Timestamps: this and next update time.
393	 * Validate that the current date falls into this interval.
394	 * This is required by section 4.4, (3).
395	 * If we're after the given date, then the MFT is stale.
396	 * This is made super complicated because it uses OpenSSL's
397	 * ASN1_GENERALIZEDTIME instead of ASN1_TIME, which we could
398	 * compare against the current time trivially.
399	 */
400
401	t = sk_ASN1_TYPE_value(seq, i++);
402	if (t->type != V_ASN1_GENERALIZEDTIME) {
403		warnx("%s: RFC 6486 section 4.2.1: thisUpdate: "
404		    "want ASN.1 generalised time, have %s (NID %d)",
405		    p->fn, ASN1_tag2str(t->type), t->type);
406		goto out;
407	}
408	from = t->value.generalizedtime;
409
410	t = sk_ASN1_TYPE_value(seq, i++);
411	if (t->type != V_ASN1_GENERALIZEDTIME) {
412		warnx("%s: RFC 6486 section 4.2.1: nextUpdate: "
413		    "want ASN.1 generalised time, have %s (NID %d)",
414		    p->fn, ASN1_tag2str(t->type), t->type);
415		goto out;
416	}
417	until = t->value.generalizedtime;
418
419	switch (check_validity(from, until, p->fn)) {
420	case 0:
421		p->res->stale = 1;
422		/* FALLTHROUGH */
423	case 1:
424		break;
425	case -1:
426		goto out;
427	}
428
429	/* File list algorithm. */
430
431	t = sk_ASN1_TYPE_value(seq, i++);
432	if (t->type != V_ASN1_OBJECT) {
433		warnx("%s: RFC 6486 section 4.2.1: fileHashAlg: "
434		    "want ASN.1 object, have %s (NID %d)",
435		    p->fn, ASN1_tag2str(t->type), t->type);
436		goto out;
437	}
438	if (OBJ_obj2nid(t->value.object) != NID_sha256) {
439		warnx("%s: RFC 6486 section 4.2.1: fileHashAlg: "
440		    "want SHA256 object, have %s (NID %d)", p->fn,
441		    ASN1_tag2str(OBJ_obj2nid(t->value.object)),
442		    OBJ_obj2nid(t->value.object));
443		goto out;
444	}
445
446	/* Now the sequence. */
447
448	t = sk_ASN1_TYPE_value(seq, i++);
449	if (t->type != V_ASN1_SEQUENCE) {
450		warnx("%s: RFC 6486 section 4.2.1: fileList: "
451		    "want ASN.1 sequence, have %s (NID %d)",
452		    p->fn, ASN1_tag2str(t->type), t->type);
453		goto out;
454	}
455
456	if (!mft_parse_flist(p, t->value.octet_string))
457		goto out;
458
459	rc = 1;
460out:
461	sk_ASN1_TYPE_pop_free(seq, ASN1_TYPE_free);
462	BN_free(mft_seqnum);
463	return rc;
464}
465
466/*
467 * Parse the objects that have been published in the manifest.
468 * This conforms to RFC 6486.
469 * Note that if the MFT is stale, all referenced objects are stripped
470 * from the parsed content.
471 * The MFT content is otherwise returned.
472 */
473struct mft *
474mft_parse(X509 **x509, const char *fn, const unsigned char *der, size_t len)
475{
476	struct parse	 p;
477	int		 rc = 0;
478	size_t		 cmsz;
479	unsigned char	*cms;
480
481	memset(&p, 0, sizeof(struct parse));
482	p.fn = fn;
483
484	cms = cms_parse_validate(x509, fn, der, len, mft_oid, &cmsz);
485	if (cms == NULL)
486		return NULL;
487	assert(*x509 != NULL);
488
489	if ((p.res = calloc(1, sizeof(struct mft))) == NULL)
490		err(1, NULL);
491
492	p.res->aia = x509_get_aia(*x509, fn);
493	p.res->aki = x509_get_aki(*x509, 0, fn);
494	p.res->ski = x509_get_ski(*x509, fn);
495	if (p.res->aia == NULL || p.res->aki == NULL || p.res->ski == NULL) {
496		warnx("%s: RFC 6487 section 4.8: "
497		    "missing AIA, AKI or SKI X509 extension", fn);
498		goto out;
499	}
500
501	if (mft_parse_econtent(cms, cmsz, &p) == 0)
502		goto out;
503
504	rc = 1;
505out:
506	if (rc == 0) {
507		mft_free(p.res);
508		p.res = NULL;
509		X509_free(*x509);
510		*x509 = NULL;
511	}
512	free(cms);
513	return p.res;
514}
515
516/*
517 * Free an MFT pointer.
518 * Safe to call with NULL.
519 */
520void
521mft_free(struct mft *p)
522{
523	size_t	 i;
524
525	if (p == NULL)
526		return;
527
528	if (p->files != NULL)
529		for (i = 0; i < p->filesz; i++)
530			free(p->files[i].file);
531
532	free(p->aia);
533	free(p->aki);
534	free(p->ski);
535	free(p->path);
536	free(p->files);
537	free(p->seqnum);
538	free(p);
539}
540
541/*
542 * Serialise MFT parsed content into the given buffer.
543 * See mft_read() for the other side of the pipe.
544 */
545void
546mft_buffer(struct ibuf *b, const struct mft *p)
547{
548	size_t		 i;
549
550	io_simple_buffer(b, &p->stale, sizeof(p->stale));
551	io_simple_buffer(b, &p->repoid, sizeof(p->repoid));
552	io_str_buffer(b, p->path);
553
554	io_str_buffer(b, p->aia);
555	io_str_buffer(b, p->aki);
556	io_str_buffer(b, p->ski);
557
558	io_simple_buffer(b, &p->filesz, sizeof(size_t));
559	for (i = 0; i < p->filesz; i++) {
560		io_str_buffer(b, p->files[i].file);
561		io_simple_buffer(b, &p->files[i].type,
562		    sizeof(p->files[i].type));
563		io_simple_buffer(b, p->files[i].hash, SHA256_DIGEST_LENGTH);
564	}
565}
566
567/*
568 * Read an MFT structure from the file descriptor.
569 * Result must be passed to mft_free().
570 */
571struct mft *
572mft_read(struct ibuf *b)
573{
574	struct mft	*p = NULL;
575	size_t		 i;
576
577	if ((p = calloc(1, sizeof(struct mft))) == NULL)
578		err(1, NULL);
579
580	io_read_buf(b, &p->stale, sizeof(p->stale));
581	io_read_buf(b, &p->repoid, sizeof(p->repoid));
582	io_read_str(b, &p->path);
583
584	io_read_str(b, &p->aia);
585	io_read_str(b, &p->aki);
586	io_read_str(b, &p->ski);
587	assert(p->aia && p->aki && p->ski);
588
589	io_read_buf(b, &p->filesz, sizeof(size_t));
590	if ((p->files = calloc(p->filesz, sizeof(struct mftfile))) == NULL)
591		err(1, NULL);
592
593	for (i = 0; i < p->filesz; i++) {
594		io_read_str(b, &p->files[i].file);
595		io_read_buf(b, &p->files[i].type, sizeof(p->files[i].type));
596		io_read_buf(b, p->files[i].hash, SHA256_DIGEST_LENGTH);
597	}
598
599	return p;
600}
601