/* c_rehash.c - Create hash symlinks for certificates * C implementation based on the original Perl and shell versions * * Copyright (c) 2013-2014 Timo Teräs * All rights reserved. * * This software is licensed under the MIT License. * Full license available at: http://opensource.org/licenses/MIT */ #include #include #include #include #include #include #include #include #include #define MAX_COLLISIONS 256 #define countof(x) (sizeof(x) / sizeof(x[0])) #if 0 #define DEBUG(args...) fprintf(stderr, args) #else #define DEBUG(args...) #endif struct entry_info { struct entry_info *next; char *filename; unsigned short old_id; unsigned char need_symlink; unsigned char digest[EVP_MAX_MD_SIZE]; }; struct bucket_info { struct bucket_info *next; struct entry_info *first_entry, *last_entry; unsigned int hash; unsigned short type; unsigned short num_needed; }; enum Type { TYPE_CERT = 0, TYPE_CRL }; static const char *symlink_extensions[] = { "", "r" }; static const char *file_extensions[] = { "pem", "crt", "cer", "crl" }; static int evpmdsize; static const EVP_MD *evpmd; static int do_hash_new = 1; static int do_hash_old = 0; static int do_remove_links = 1; static int do_verbose = 0; static struct bucket_info *hash_table[257]; static void bit_set(unsigned char *set, unsigned bit) { set[bit / 8] |= 1 << (bit % 8); } static int bit_isset(unsigned char *set, unsigned bit) { return set[bit / 8] & (1 << (bit % 8)); } static void add_entry( int type, unsigned int hash, const char *filename, const unsigned char *digest, int need_symlink, unsigned short old_id) { struct bucket_info *bi; struct entry_info *ei, *found = NULL; unsigned int ndx = (type + hash) % countof(hash_table); for (bi = hash_table[ndx]; bi; bi = bi->next) if (bi->type == type && bi->hash == hash) break; if (!bi) { bi = calloc(1, sizeof(*bi)); if (!bi) return; bi->next = hash_table[ndx]; bi->type = type; bi->hash = hash; hash_table[ndx] = bi; } for (ei = bi->first_entry; ei; ei = ei->next) { if (digest && memcmp(digest, ei->digest, evpmdsize) == 0) { fprintf(stderr, "WARNING: Skipping duplicate certificate in file %s\n", filename); return; } if (!strcmp(filename, ei->filename)) { found = ei; if (!digest) break; } } ei = found; if (!ei) { if (bi->num_needed >= MAX_COLLISIONS) return; ei = calloc(1, sizeof(*ei)); if (!ei) return; ei->old_id = ~0; ei->filename = strdup(filename); if (bi->last_entry) bi->last_entry->next = ei; if (!bi->first_entry) bi->first_entry = ei; bi->last_entry = ei; } if (old_id < ei->old_id) ei->old_id = old_id; if (need_symlink && !ei->need_symlink) { ei->need_symlink = 1; bi->num_needed++; memcpy(ei->digest, digest, evpmdsize); } } static int handle_symlink(const char *filename, const char *fullpath) { static char xdigit[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1,-1,-1,-1,-1, -1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,10,11,12,13,14,15 }; char linktarget[NAME_MAX], *endptr; unsigned int hash = 0; unsigned char ch; int i, type, id; ssize_t n; for (i = 0; i < 8; i++) { ch = filename[i] - '0'; if (ch >= countof(xdigit) || xdigit[ch] < 0) return -1; hash <<= 4; hash += xdigit[ch]; } if (filename[i++] != '.') return -1; for (type = countof(symlink_extensions) - 1; type > 0; type--) if (strcasecmp(symlink_extensions[type], &filename[i]) == 0) break; i += strlen(symlink_extensions[type]); id = strtoul(&filename[i], &endptr, 10); if (*endptr != 0) return -1; n = readlink(fullpath, linktarget, sizeof(linktarget)); if (n >= sizeof(linktarget) || n < 0) return -1; linktarget[n] = 0; DEBUG("Found existing symlink %s for %08x (%d), certname %s\n", filename, hash, type, linktarget); add_entry(type, hash, linktarget, NULL, 0, id); return 0; } static int handle_certificate(const char *filename, const char *fullpath) { STACK_OF(X509_INFO) *inf; X509_INFO *x; BIO *b; const char *ext; unsigned char digest[EVP_MAX_MD_SIZE]; X509_NAME *name = NULL; int i, type, ret = -1; ext = strrchr(filename, '.'); if (ext == NULL) return 0; for (i = 0; i < countof(file_extensions); i++) { if (strcasecmp(file_extensions[i], ext+1) == 0) break; } if (i >= countof(file_extensions)) return -1; b = BIO_new_file(fullpath, "r"); if (!b) return -1; inf = PEM_X509_INFO_read_bio(b, NULL, NULL, NULL); BIO_free(b); if (!inf) return -1; if (sk_X509_INFO_num(inf) == 1) { x = sk_X509_INFO_value(inf, 0); if (x->x509) { type = TYPE_CERT; name = X509_get_subject_name(x->x509); X509_digest(x->x509, evpmd, digest, NULL); } else if (x->crl) { type = TYPE_CRL; name = X509_CRL_get_issuer(x->crl); X509_CRL_digest(x->crl, evpmd, digest, NULL); } if (name && do_hash_new) add_entry(type, X509_NAME_hash(name), filename, digest, 1, ~0); if (name && do_hash_old) add_entry(type, X509_NAME_hash_old(name), filename, digest, 1, ~0); } else { fprintf(stderr, "WARNING: %s does not contain exactly one certificate or CRL: skipping\n", filename); } sk_X509_INFO_pop_free(inf, X509_INFO_free); return ret; } static int hash_dir(const char *dirname) { struct bucket_info *bi, *nextbi; struct entry_info *ei, *nextei; struct dirent *de; struct stat st; unsigned char idmask[MAX_COLLISIONS / 8]; int i, n, nextid, buflen, ret = -1; const char *pathsep; char *buf; DIR *d; if (access(dirname, R_OK|W_OK|X_OK) != 0) { fprintf(stderr, "ERROR: Access denied '%s'\n", dirname); return -1; } buflen = strlen(dirname); pathsep = (buflen && dirname[buflen-1] == '/') ? "" : "/"; buflen += NAME_MAX + 2; buf = malloc(buflen); if (buf == NULL) goto err; if (do_verbose) printf("Doing %s\n", dirname); d = opendir(dirname); if (!d) goto err; while ((de = readdir(d)) != NULL) { if (snprintf(buf, buflen, "%s%s%s", dirname, pathsep, de->d_name) >= buflen) continue; if (lstat(buf, &st) < 0) continue; if (S_ISLNK(st.st_mode) && handle_symlink(de->d_name, buf) == 0) continue; handle_certificate(de->d_name, buf); } closedir(d); for (i = 0; i < countof(hash_table); i++) { for (bi = hash_table[i]; bi; bi = nextbi) { nextbi = bi->next; DEBUG("Type %d, hash %08x, num entries %d:\n", bi->type, bi->hash, bi->num_needed); nextid = 0; memset(idmask, 0, (bi->num_needed+7)/8); for (ei = bi->first_entry; ei; ei = ei->next) if (ei->old_id < bi->num_needed) bit_set(idmask, ei->old_id); for (ei = bi->first_entry; ei; ei = nextei) { nextei = ei->next; DEBUG("\t(old_id %d, need_symlink %d) Cert %s\n", ei->old_id, ei->need_symlink, ei->filename); if (ei->old_id < bi->num_needed) { /* Link exists, and is used as-is */ snprintf(buf, buflen, "%08x.%s%d", bi->hash, symlink_extensions[bi->type], ei->old_id); if (do_verbose) printf("link %s -> %s\n", ei->filename, buf); } else if (ei->need_symlink) { /* New link needed (it may replace something) */ while (bit_isset(idmask, nextid)) nextid++; snprintf(buf, buflen, "%s%s%n%08x.%s%d", dirname, pathsep, &n, bi->hash, symlink_extensions[bi->type], nextid); if (do_verbose) printf("link %s -> %s\n", ei->filename, &buf[n]); unlink(buf); symlink(ei->filename, buf); } else if (do_remove_links) { /* Link to be deleted */ snprintf(buf, buflen, "%s%s%n%08x.%s%d", dirname, pathsep, &n, bi->hash, symlink_extensions[bi->type], ei->old_id); if (do_verbose) printf("unlink %s\n", &buf[n]); unlink(buf); } free(ei->filename); free(ei); } free(bi); } hash_table[i] = NULL; } ret = 0; err: free(buf); return ret; } static void c_rehash_usage(void) { printf("\ usage: c_rehash \n\ \n\ -compat - create new- and old-style hashed links\n\ -old - use old-style hashing for generating links\n\ -h - display this help\n\ -n - do not remove existing links\n\ -v - be more verbose\n\ \n"); } int main(int argc, char **argv) { const char *env, *opt; int i, numargs, r = 0; evpmd = EVP_sha1(); evpmdsize = EVP_MD_size(evpmd); numargs = argc; for (i = 1; i < argc; i++) { if (argv[i][0] != '-') continue; if (strcmp(argv[i], "--") == 0) { argv[i] = 0; numargs--; break; } opt = &argv[i][1]; if (strcmp(opt, "compat") == 0) { do_hash_new = do_hash_old = 1; } else if (strcmp(opt, "old") == 0) { do_hash_new = 0; do_hash_old = 1; } else if (strcmp(opt, "n") == 0) { do_remove_links = 0; } else if (strcmp(opt, "v") == 0) { do_verbose++; } else { if (strcmp(opt, "h") != 0) fprintf(stderr, "unknown option %s\n", argv[i]); c_rehash_usage(); return 1; } argv[i] = 0; numargs--; } if (numargs > 1) { for (i = 1; i < argc; i++) if (argv[i]) r |= hash_dir(argv[i]); } else if ((env = getenv("SSL_CERT_DIR")) != NULL) { char *e, *m; m = strdup(env); for (e = strtok(m, ":"); e != NULL; e = strtok(NULL, ":")) r |= hash_dir(e); free(m); } else { r |= hash_dir("/etc/ssl/certs"); } return r ? 2 : 0; }