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