1 /* $OpenBSD: certhash.c,v 1.19 2021/10/23 08:13:48 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 } certhash_config; 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 = &certhash_config.dryrun, 47 }, 48 { 49 .name = "v", 50 .desc = "Verbose", 51 .type = OPTION_FLAG, 52 .opt.flag = &certhash_config.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 * 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 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 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 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 * 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 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 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 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 * 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 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 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 * 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 * 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 * 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 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 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 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 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 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 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 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 (certhash_config.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 (certhash_config.verbose) 598 fprintf(stdout, "%s link %s -> %s\n", 599 (certhash_config.dryrun ? "would remove" : 600 "removing"), link->filename, link->target); 601 if (certhash_config.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 (certhash_config.verbose) 615 fprintf(stdout, "%s link %s -> %s\n", 616 (certhash_config.dryrun ? "would create" : 617 "creating"), link->filename, 618 link->reference->filename); 619 if (certhash_config.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 644 certhash_usage(void) 645 { 646 fprintf(stderr, "usage: certhash [-nv] dir ...\n"); 647 options_usage(certhash_options); 648 } 649 650 int 651 certhash_main(int argc, char **argv) 652 { 653 int argsused; 654 int i, cwdfd, ret = 0; 655 656 if (single_execution) { 657 if (pledge("stdio cpath wpath rpath", NULL) == -1) { 658 perror("pledge"); 659 exit(1); 660 } 661 } 662 663 memset(&certhash_config, 0, sizeof(certhash_config)); 664 665 if (options_parse(argc, argv, certhash_options, NULL, &argsused) != 0) { 666 certhash_usage(); 667 return (1); 668 } 669 670 if ((cwdfd = open(".", O_RDONLY)) == -1) { 671 perror("failed to open current directory"); 672 return (1); 673 } 674 675 for (i = argsused; i < argc; i++) { 676 if (chdir(argv[i]) == -1) { 677 fprintf(stderr, 678 "failed to change to directory %s: %s\n", 679 argv[i], strerror(errno)); 680 ret = 1; 681 continue; 682 } 683 ret |= certhash_directory(argv[i]); 684 if (fchdir(cwdfd) == -1) { 685 perror("failed to restore current directory"); 686 ret = 1; 687 break; /* can't continue safely */ 688 } 689 } 690 close(cwdfd); 691 692 return (ret); 693 } 694