1 /*
2  * Copyright (c) 2014, 2015 Joel Sing <jsing@openbsd.org>
3  *
4  * Permission to use, copy, modify, and distribute this software for any
5  * purpose with or without fee is hereby granted, provided that the above
6  * copyright notice and this permission notice appear in all copies.
7  *
8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15  */
16 
17 #include <sys/types.h>
18 #include <sys/stat.h>
19 
20 #include <errno.h>
21 #include <dirent.h>
22 #include <fcntl.h>
23 #include <limits.h>
24 #include <stdio.h>
25 #include <string.h>
26 #include <unistd.h>
27 
28 #include <openssl/bio.h>
29 #include <openssl/evp.h>
30 #include <openssl/pem.h>
31 #include <openssl/x509.h>
32 
33 #include "apps.h"
34 
35 static struct {
36 	int dryrun;
37 	int verbose;
38 } certhash_config;
39 
40 struct option certhash_options[] = {
41 	{
42 		.name = "n",
43 		.desc = "Perform a dry-run - do not make any changes",
44 		.type = OPTION_FLAG,
45 		.opt.flag = &certhash_config.dryrun,
46 	},
47 	{
48 		.name = "v",
49 		.desc = "Verbose",
50 		.type = OPTION_FLAG,
51 		.opt.flag = &certhash_config.verbose,
52 	},
53 	{ NULL },
54 };
55 
56 struct hashinfo {
57 	char *filename;
58 	char *target;
59 	unsigned long hash;
60 	unsigned int index;
61 	unsigned char fingerprint[EVP_MAX_MD_SIZE];
62 	int is_crl;
63 	int is_dup;
64 	int exists;
65 	int changed;
66 	struct hashinfo *reference;
67 	struct hashinfo *next;
68 };
69 
70 static struct hashinfo *
71 hashinfo(const char *filename, unsigned long hash, unsigned char *fingerprint)
72 {
73 	struct hashinfo *hi;
74 
75 	if ((hi = calloc(1, sizeof(*hi))) == NULL)
76 		return (NULL);
77 	if (filename != NULL) {
78 		if ((hi->filename = strdup(filename)) == NULL) {
79 			free(hi);
80 			return (NULL);
81 		}
82 	}
83 	hi->hash = hash;
84 	if (fingerprint != NULL)
85 		memcpy(hi->fingerprint, fingerprint, sizeof(hi->fingerprint));
86 
87 	return (hi);
88 }
89 
90 static void
91 hashinfo_free(struct hashinfo *hi)
92 {
93 	if (hi == NULL)
94 		return;
95 
96 	free(hi->filename);
97 	free(hi->target);
98 	free(hi);
99 }
100 
101 #ifdef DEBUG
102 static void
103 hashinfo_print(struct hashinfo *hi)
104 {
105 	int i;
106 
107 	printf("hashinfo %s %08lx %u %i\n", hi->filename, hi->hash,
108 	    hi->index, hi->is_crl);
109 	for (i = 0; i < (int)EVP_MAX_MD_SIZE; i++) {
110 		printf("%02X%c", hi->fingerprint[i],
111 		    (i + 1 == (int)EVP_MAX_MD_SIZE) ? '\n' : ':');
112 	}
113 }
114 #endif
115 
116 static int
117 hashinfo_compare(const void *a, const void *b)
118 {
119 	struct hashinfo *hia = *(struct hashinfo **)a;
120 	struct hashinfo *hib = *(struct hashinfo **)b;
121 	int rv;
122 
123 	rv = hia->hash < hib->hash ? -1 : hia->hash > hib->hash;
124 	if (rv != 0)
125 		return (rv);
126 	rv = memcmp(hia->fingerprint, hib->fingerprint,
127 	    sizeof(hia->fingerprint));
128 	if (rv != 0)
129 		return (rv);
130 	return strcmp(hia->filename, hib->filename);
131 }
132 
133 static struct hashinfo *
134 hashinfo_chain(struct hashinfo *head, struct hashinfo *entry)
135 {
136 	struct hashinfo *hi = head;
137 
138 	if (hi == NULL)
139 		return (entry);
140 	while (hi->next != NULL)
141 		hi = hi->next;
142 	hi->next = entry;
143 
144 	return (head);
145 }
146 
147 static void
148 hashinfo_chain_free(struct hashinfo *hi)
149 {
150 	struct hashinfo *next;
151 
152 	while (hi != NULL) {
153 		next = hi->next;
154 		hashinfo_free(hi);
155 		hi = next;
156 	}
157 }
158 
159 static size_t
160 hashinfo_chain_length(struct hashinfo *hi)
161 {
162 	int len = 0;
163 
164 	while (hi != NULL) {
165 		len++;
166 		hi = hi->next;
167 	}
168 	return (len);
169 }
170 
171 static int
172 hashinfo_chain_sort(struct hashinfo **head)
173 {
174 	struct hashinfo **list, *entry;
175 	size_t len;
176 	int i;
177 
178 	if (*head == NULL)
179 		return (0);
180 
181 	len = hashinfo_chain_length(*head);
182 	if ((list = reallocarray(NULL, len, sizeof(struct hashinfo *))) == NULL)
183 		return (-1);
184 
185 	for (entry = *head, i = 0; entry != NULL; entry = entry->next, i++)
186 		list[i] = entry;
187 	qsort(list, len, sizeof(struct hashinfo *), hashinfo_compare);
188 
189 	*head = entry = list[0];
190 	for (i = 1; i < len; i++) {
191 		entry->next = list[i];
192 		entry = list[i];
193 	}
194 	entry->next = NULL;
195 
196 	free(list);
197 	return (0);
198 }
199 
200 static char *
201 hashinfo_linkname(struct hashinfo *hi)
202 {
203 	char *filename;
204 
205 	if (asprintf(&filename, "%08lx.%s%u", hi->hash,
206 	    (hi->is_crl ? "r" : ""), hi->index) == -1)
207 		return (NULL);
208 
209 	return (filename);
210 }
211 
212 static int
213 filename_is_hash(const char *filename)
214 {
215 	const char *p = filename;
216 
217 	while ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f'))
218 		p++;
219 	if (*p++ != '.')
220 		return (0);
221 	if (*p == 'r')		/* CRL format. */
222 		p++;
223 	while (*p >= '0' && *p <= '9')
224 		p++;
225 	if (*p != '\0')
226 		return (0);
227 
228 	return (1);
229 }
230 
231 static int
232 filename_is_pem(const char *filename)
233 {
234 	const char *q, *p = filename;
235 
236 	if ((q = strchr(p, '\0')) == NULL)
237 		return (0);
238 	if ((q - p) < 4)
239 		return (0);
240 	if (strncmp((q - 4), ".pem", 4) != 0)
241 		return (0);
242 
243 	return (1);
244 }
245 
246 static struct hashinfo *
247 hashinfo_from_linkname(const char *linkname, const char *target)
248 {
249 	struct hashinfo *hi = NULL;
250 	const char *errstr;
251 	char *l, *p, *ep;
252 	long long val;
253 
254 	if ((l = strdup(linkname)) == NULL)
255 		goto err;
256 	if ((p = strchr(l, '.')) == NULL)
257 		goto err;
258 	*p++ = '\0';
259 
260 	if ((hi = hashinfo(linkname, 0, NULL)) == NULL)
261 		goto err;
262 	if ((hi->target = strdup(target)) == NULL)
263 		goto err;
264 
265 	errno = 0;
266 	val = strtoll(l, &ep, 16);
267 	if (l[0] == '\0' || *ep != '\0')
268 		goto err;
269 	if (errno == ERANGE && (val == LLONG_MAX || val == LLONG_MIN))
270 		goto err;
271 	if (val < 0 || val > ULONG_MAX)
272 		goto err;
273 	hi->hash = (unsigned long)val;
274 
275 	if (*p == 'r') {
276 		hi->is_crl = 1;
277 		p++;
278 	}
279 
280 	val = strtonum(p, 0, 0xffffffff, &errstr);
281 	if (errstr != NULL)
282 		goto err;
283 
284 	hi->index = (unsigned int)val;
285 
286 	goto done;
287 
288  err:
289 	hashinfo_free(hi);
290 	hi = NULL;
291 
292  done:
293 	free(l);
294 
295 	return (hi);
296 }
297 
298 static struct hashinfo *
299 certhash_cert(BIO *bio, const char *filename)
300 {
301 	unsigned char fingerprint[EVP_MAX_MD_SIZE];
302 	struct hashinfo *hi = NULL;
303 	const EVP_MD *digest;
304 	X509 *cert = NULL;
305 	unsigned long hash;
306 	unsigned int len;
307 
308 	if ((cert = PEM_read_bio_X509(bio, NULL, NULL, NULL)) == NULL)
309 		goto err;
310 
311 	hash = X509_subject_name_hash(cert);
312 
313 	digest = EVP_sha256();
314 	if (X509_digest(cert, digest, fingerprint, &len) != 1) {
315 		fprintf(stderr, "out of memory\n");
316 		goto err;
317 	}
318 
319 	hi = hashinfo(filename, hash, fingerprint);
320 
321  err:
322 	X509_free(cert);
323 
324 	return (hi);
325 }
326 
327 static struct hashinfo *
328 certhash_crl(BIO *bio, const char *filename)
329 {
330 	unsigned char fingerprint[EVP_MAX_MD_SIZE];
331 	struct hashinfo *hi = NULL;
332 	const EVP_MD *digest;
333 	X509_CRL *crl = NULL;
334 	unsigned long hash;
335 	unsigned int len;
336 
337 	if ((crl = PEM_read_bio_X509_CRL(bio, NULL, NULL, NULL)) == NULL)
338 		return (NULL);
339 
340 	hash = X509_NAME_hash(X509_CRL_get_issuer(crl));
341 
342 	digest = EVP_sha256();
343 	if (X509_CRL_digest(crl, digest, fingerprint, &len) != 1) {
344 		fprintf(stderr, "out of memory\n");
345 		goto err;
346 	}
347 
348 	hi = hashinfo(filename, hash, fingerprint);
349 
350  err:
351 	X509_CRL_free(crl);
352 
353 	return (hi);
354 }
355 
356 static int
357 certhash_addlink(struct hashinfo **links, struct hashinfo *hi)
358 {
359 	struct hashinfo *link = NULL;
360 
361 	if ((link = hashinfo(NULL, hi->hash, hi->fingerprint)) == NULL)
362 		goto err;
363 
364 	if ((link->filename = hashinfo_linkname(hi)) == NULL)
365 		goto err;
366 
367 	link->reference = hi;
368 	link->changed = 1;
369 	*links = hashinfo_chain(*links, link);
370 	hi->reference = link;
371 
372 	return (0);
373 
374  err:
375 	hashinfo_free(link);
376 	return (-1);
377 }
378 
379 static void
380 certhash_findlink(struct hashinfo *links, struct hashinfo *hi)
381 {
382 	struct hashinfo *link;
383 
384 	for (link = links; link != NULL; link = link->next) {
385 		if (link->is_crl == hi->is_crl &&
386 		    link->hash == hi->hash &&
387 		    link->index == hi->index &&
388 		    link->reference == NULL) {
389 			link->reference = hi;
390 			if (link->target == NULL ||
391 			    strcmp(link->target, hi->filename) != 0)
392 				link->changed = 1;
393 			hi->reference = link;
394 			break;
395 		}
396 	}
397 }
398 
399 static void
400 certhash_index(struct hashinfo *head, const char *name)
401 {
402 	struct hashinfo *last, *entry;
403 	int index = 0;
404 
405 	last = NULL;
406 	for (entry = head; entry != NULL; entry = entry->next) {
407 		if (last != NULL) {
408 			if (entry->hash == last->hash) {
409 				if (memcmp(entry->fingerprint,
410 				    last->fingerprint,
411 				    sizeof(entry->fingerprint)) == 0) {
412 					fprintf(stderr, "WARNING: duplicate %s "
413 					    "in %s (using %s), ignoring...\n",
414 					    name, entry->filename,
415 					    last->filename);
416 					entry->is_dup = 1;
417 					continue;
418 				}
419 				index++;
420 			} else {
421 				index = 0;
422 			}
423 		}
424 		entry->index = index;
425 		last = entry;
426 	}
427 }
428 
429 static int
430 certhash_merge(struct hashinfo **links, struct hashinfo **certs,
431     struct hashinfo **crls)
432 {
433 	struct hashinfo *cert, *crl;
434 
435 	/* Pass 1 - sort and index entries. */
436 	if (hashinfo_chain_sort(certs) == -1)
437 		return (-1);
438 	if (hashinfo_chain_sort(crls) == -1)
439 		return (-1);
440 	certhash_index(*certs, "certificate");
441 	certhash_index(*crls, "CRL");
442 
443 	/* Pass 2 - map to existing links. */
444 	for (cert = *certs; cert != NULL; cert = cert->next) {
445 		if (cert->is_dup == 1)
446 			continue;
447 		certhash_findlink(*links, cert);
448 	}
449 	for (crl = *crls; crl != NULL; crl = crl->next) {
450 		if (crl->is_dup == 1)
451 			continue;
452 		certhash_findlink(*links, crl);
453 	}
454 
455 	/* Pass 3 - determine missing links. */
456 	for (cert = *certs; cert != NULL; cert = cert->next) {
457 		if (cert->is_dup == 1 || cert->reference != NULL)
458 			continue;
459 		if (certhash_addlink(links, cert) == -1)
460 			return (-1);
461 	}
462 	for (crl = *crls; crl != NULL; crl = crl->next) {
463 		if (crl->is_dup == 1 || crl->reference != NULL)
464 			continue;
465 		if (certhash_addlink(links, crl) == -1)
466 			return (-1);
467 	}
468 
469 	return (0);
470 }
471 
472 static int
473 certhash_link(struct dirent *dep, struct hashinfo **links)
474 {
475 	struct hashinfo *hi = NULL;
476 	char target[PATH_MAX];
477 	struct stat sb;
478 	int n;
479 
480 	if (lstat(dep->d_name, &sb) == -1) {
481 		fprintf(stderr, "failed to stat %s\n", dep->d_name);
482 		return (-1);
483 	}
484 	if (!S_ISLNK(sb.st_mode))
485 		return (0);
486 
487 	n = readlink(dep->d_name, target, sizeof(target) - 1);
488 	if (n == -1) {
489 		fprintf(stderr, "failed to readlink %s\n", dep->d_name);
490 		return (-1);
491 	}
492 	target[n] = '\0';
493 
494 	hi = hashinfo_from_linkname(dep->d_name, target);
495 	if (hi == NULL) {
496 		fprintf(stderr, "failed to get hash info %s\n", dep->d_name);
497 		return (-1);
498 	}
499 	hi->exists = 1;
500 	*links = hashinfo_chain(*links, hi);
501 
502 	return (0);
503 }
504 
505 static int
506 certhash_file(struct dirent *dep, struct hashinfo **certs,
507     struct hashinfo **crls)
508 {
509 	struct hashinfo *hi = NULL;
510 	int has_cert, has_crl;
511 	int ret = -1;
512 	BIO *bio = NULL;
513 	FILE *f;
514 
515 	has_cert = has_crl = 0;
516 
517 	if ((f = fopen(dep->d_name, "r")) == NULL) {
518 		fprintf(stderr, "failed to fopen %s\n", dep->d_name);
519 		goto err;
520 	}
521 	if ((bio = BIO_new_fp(f, BIO_CLOSE)) == NULL) {
522 		fprintf(stderr, "failed to create bio\n");
523 		fclose(f);
524 		goto err;
525 	}
526 
527 	if ((hi = certhash_cert(bio, dep->d_name)) != NULL) {
528 		has_cert = 1;
529 		*certs = hashinfo_chain(*certs, hi);
530 	}
531 
532 	if (BIO_reset(bio) != 0) {
533 		fprintf(stderr, "BIO_reset failed\n");
534 		goto err;
535 	}
536 
537 	if ((hi = certhash_crl(bio, dep->d_name)) != NULL) {
538 		has_crl = hi->is_crl = 1;
539 		*crls = hashinfo_chain(*crls, hi);
540 	}
541 
542 	if (!has_cert && !has_crl)
543 		fprintf(stderr, "PEM file %s does not contain a certificate "
544 		    "or CRL, ignoring...\n", dep->d_name);
545 
546 	ret = 0;
547 
548  err:
549 	BIO_free(bio);
550 
551 	return (ret);
552 }
553 
554 static int
555 certhash_directory(const char *path)
556 {
557 	struct hashinfo *links = NULL, *certs = NULL, *crls = NULL, *link;
558 	int ret = 0;
559 	struct dirent *dep;
560 	DIR *dip = NULL;
561 
562 	if ((dip = opendir(".")) == NULL) {
563 		fprintf(stderr, "failed to open directory %s\n", path);
564 		goto err;
565 	}
566 
567 	if (certhash_config.verbose)
568 		fprintf(stdout, "scanning directory %s\n", path);
569 
570 	/* Create lists of existing hash links, certs and CRLs. */
571 	while ((dep = readdir(dip)) != NULL) {
572 		if (filename_is_hash(dep->d_name)) {
573 			if (certhash_link(dep, &links) == -1)
574 				goto err;
575 		}
576 		if (filename_is_pem(dep->d_name)) {
577 			if (certhash_file(dep, &certs, &crls) == -1)
578 				goto err;
579 		}
580 	}
581 
582 	if (certhash_merge(&links, &certs, &crls) == -1) {
583 		fprintf(stderr, "certhash merge failed\n");
584 		goto err;
585 	}
586 
587 	/* Remove spurious links. */
588 	for (link = links; link != NULL; link = link->next) {
589 		if (link->exists == 0 ||
590 		    (link->reference != NULL && link->changed == 0))
591 			continue;
592 		if (certhash_config.verbose)
593 			fprintf(stdout, "%s link %s -> %s\n",
594 			    (certhash_config.dryrun ? "would remove" :
595 				"removing"), link->filename, link->target);
596 		if (certhash_config.dryrun)
597 			continue;
598 		if (unlink(link->filename) == -1) {
599 			fprintf(stderr, "failed to remove link %s\n",
600 			    link->filename);
601 			goto err;
602 		}
603 	}
604 
605 	/* Create missing links. */
606 	for (link = links; link != NULL; link = link->next) {
607 		if (link->exists == 1 && link->changed == 0)
608 			continue;
609 		if (certhash_config.verbose)
610 			fprintf(stdout, "%s link %s -> %s\n",
611 			    (certhash_config.dryrun ? "would create" :
612 				"creating"), link->filename,
613 			    link->reference->filename);
614 		if (certhash_config.dryrun)
615 			continue;
616 		if (symlink(link->reference->filename, link->filename) == -1) {
617 			fprintf(stderr, "failed to create link %s -> %s\n",
618 			    link->filename, link->reference->filename);
619 			goto err;
620 		}
621 	}
622 
623 	goto done;
624 
625  err:
626 	ret = 1;
627 
628  done:
629 	hashinfo_chain_free(certs);
630 	hashinfo_chain_free(crls);
631 	hashinfo_chain_free(links);
632 
633 	if (dip != NULL)
634 		closedir(dip);
635 	return (ret);
636 }
637 
638 static void
639 certhash_usage(void)
640 {
641 	fprintf(stderr, "usage: certhash [-nv] dir ...\n");
642 	options_usage(certhash_options);
643 }
644 
645 int
646 certhash_main(int argc, char **argv)
647 {
648 	int argsused;
649 	int i, cwdfd, ret = 0;
650 
651 	if (single_execution) {
652 		if (pledge("stdio cpath wpath rpath", NULL) == -1) {
653 			perror("pledge");
654 			exit(1);
655 		}
656 	}
657 
658 	memset(&certhash_config, 0, sizeof(certhash_config));
659 
660 	if (options_parse(argc, argv, certhash_options, NULL, &argsused) != 0) {
661                 certhash_usage();
662                 return (1);
663         }
664 
665 	if ((cwdfd = open(".", O_RDONLY)) == -1) {
666 		perror("failed to open current directory");
667 		return (1);
668 	}
669 
670 	for (i = argsused; i < argc; i++) {
671 		if (chdir(argv[i]) == -1) {
672 			fprintf(stderr,
673 			    "failed to change to directory %s: %s\n",
674 			    argv[i], strerror(errno));
675 			ret = 1;
676 			continue;
677 		}
678 		ret |= certhash_directory(argv[i]);
679 		if (fchdir(cwdfd) == -1) {
680 			perror("failed to restore current directory");
681 			ret = 1;
682 			break;		/* can't continue safely */
683 		}
684 	}
685 	close(cwdfd);
686 
687 	return (ret);
688 }
689