1 /* $OpenBSD: certhash.c,v 1.18 2021/08/28 08:16:39 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 target[n] = '\0'; 494 495 hi = hashinfo_from_linkname(dep->d_name, target); 496 if (hi == NULL) { 497 fprintf(stderr, "failed to get hash info %s\n", dep->d_name); 498 return (-1); 499 } 500 hi->exists = 1; 501 *links = hashinfo_chain(*links, hi); 502 503 return (0); 504 } 505 506 static int 507 certhash_file(struct dirent *dep, struct hashinfo **certs, 508 struct hashinfo **crls) 509 { 510 struct hashinfo *hi = NULL; 511 int has_cert, has_crl; 512 int ret = -1; 513 BIO *bio = NULL; 514 FILE *f; 515 516 has_cert = has_crl = 0; 517 518 if ((f = fopen(dep->d_name, "r")) == NULL) { 519 fprintf(stderr, "failed to fopen %s\n", dep->d_name); 520 goto err; 521 } 522 if ((bio = BIO_new_fp(f, BIO_CLOSE)) == NULL) { 523 fprintf(stderr, "failed to create bio\n"); 524 fclose(f); 525 goto err; 526 } 527 528 if ((hi = certhash_cert(bio, dep->d_name)) != NULL) { 529 has_cert = 1; 530 *certs = hashinfo_chain(*certs, hi); 531 } 532 533 if (BIO_reset(bio) != 0) { 534 fprintf(stderr, "BIO_reset failed\n"); 535 goto err; 536 } 537 538 if ((hi = certhash_crl(bio, dep->d_name)) != NULL) { 539 has_crl = hi->is_crl = 1; 540 *crls = hashinfo_chain(*crls, hi); 541 } 542 543 if (!has_cert && !has_crl) 544 fprintf(stderr, "PEM file %s does not contain a certificate " 545 "or CRL, ignoring...\n", dep->d_name); 546 547 ret = 0; 548 549 err: 550 BIO_free(bio); 551 552 return (ret); 553 } 554 555 static int 556 certhash_directory(const char *path) 557 { 558 struct hashinfo *links = NULL, *certs = NULL, *crls = NULL, *link; 559 int ret = 0; 560 struct dirent *dep; 561 DIR *dip = NULL; 562 563 if ((dip = opendir(".")) == NULL) { 564 fprintf(stderr, "failed to open directory %s\n", path); 565 goto err; 566 } 567 568 if (certhash_config.verbose) 569 fprintf(stdout, "scanning directory %s\n", path); 570 571 /* Create lists of existing hash links, certs and CRLs. */ 572 while ((dep = readdir(dip)) != NULL) { 573 if (filename_is_hash(dep->d_name)) { 574 if (certhash_link(dep, &links) == -1) 575 goto err; 576 } 577 if (filename_is_pem(dep->d_name)) { 578 if (certhash_file(dep, &certs, &crls) == -1) 579 goto err; 580 } 581 } 582 583 if (certhash_merge(&links, &certs, &crls) == -1) { 584 fprintf(stderr, "certhash merge failed\n"); 585 goto err; 586 } 587 588 /* Remove spurious links. */ 589 for (link = links; link != NULL; link = link->next) { 590 if (link->exists == 0 || 591 (link->reference != NULL && link->changed == 0)) 592 continue; 593 if (certhash_config.verbose) 594 fprintf(stdout, "%s link %s -> %s\n", 595 (certhash_config.dryrun ? "would remove" : 596 "removing"), link->filename, link->target); 597 if (certhash_config.dryrun) 598 continue; 599 if (unlink(link->filename) == -1) { 600 fprintf(stderr, "failed to remove link %s\n", 601 link->filename); 602 goto err; 603 } 604 } 605 606 /* Create missing links. */ 607 for (link = links; link != NULL; link = link->next) { 608 if (link->exists == 1 && link->changed == 0) 609 continue; 610 if (certhash_config.verbose) 611 fprintf(stdout, "%s link %s -> %s\n", 612 (certhash_config.dryrun ? "would create" : 613 "creating"), link->filename, 614 link->reference->filename); 615 if (certhash_config.dryrun) 616 continue; 617 if (symlink(link->reference->filename, link->filename) == -1) { 618 fprintf(stderr, "failed to create link %s -> %s\n", 619 link->filename, link->reference->filename); 620 goto err; 621 } 622 } 623 624 goto done; 625 626 err: 627 ret = 1; 628 629 done: 630 hashinfo_chain_free(certs); 631 hashinfo_chain_free(crls); 632 hashinfo_chain_free(links); 633 634 if (dip != NULL) 635 closedir(dip); 636 return (ret); 637 } 638 639 static void 640 certhash_usage(void) 641 { 642 fprintf(stderr, "usage: certhash [-nv] dir ...\n"); 643 options_usage(certhash_options); 644 } 645 646 int 647 certhash_main(int argc, char **argv) 648 { 649 int argsused; 650 int i, cwdfd, ret = 0; 651 652 if (single_execution) { 653 if (pledge("stdio cpath wpath rpath", NULL) == -1) { 654 perror("pledge"); 655 exit(1); 656 } 657 } 658 659 memset(&certhash_config, 0, sizeof(certhash_config)); 660 661 if (options_parse(argc, argv, certhash_options, NULL, &argsused) != 0) { 662 certhash_usage(); 663 return (1); 664 } 665 666 if ((cwdfd = open(".", O_RDONLY)) == -1) { 667 perror("failed to open current directory"); 668 return (1); 669 } 670 671 for (i = argsused; i < argc; i++) { 672 if (chdir(argv[i]) == -1) { 673 fprintf(stderr, 674 "failed to change to directory %s: %s\n", 675 argv[i], strerror(errno)); 676 ret = 1; 677 continue; 678 } 679 ret |= certhash_directory(argv[i]); 680 if (fchdir(cwdfd) == -1) { 681 perror("failed to restore current directory"); 682 ret = 1; 683 break; /* can't continue safely */ 684 } 685 } 686 close(cwdfd); 687 688 return (ret); 689 } 690