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