[BACK]Return to certhash.c CVS log [TXT][DIR] Up to [local] / src / usr.bin / openssl

File: [local] / src / usr.bin / openssl / certhash.c (download)

Revision 1.21, Mon Mar 6 14:32:05 2023 UTC (14 months, 2 weeks ago) by tb
Branch: MAIN
CVS Tags: OPENBSD_7_5_BASE, OPENBSD_7_5, OPENBSD_7_4_BASE, OPENBSD_7_4, OPENBSD_7_3_BASE, OPENBSD_7_3, HEAD
Changes since 1.20: +12 -12 lines

Rename struct ${app}_config to plain cfg

All the structs are static and we need to reach into them many times.
Having a shorter name is more concise and results in less visual clutter.
It also avoids many overlong lines and we will be able to get rid of some
unfortunate line wrapping down the road.

Discussed with jsing

/*	$OpenBSD: certhash.c,v 1.21 2023/03/06 14:32:05 tb Exp $ */
/*
 * Copyright (c) 2014, 2015 Joel Sing <jsing@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>
#include <sys/stat.h>

#include <errno.h>
#include <dirent.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/x509.h>

#include "apps.h"

static struct {
	int dryrun;
	int verbose;
} cfg;

static const struct option certhash_options[] = {
	{
		.name = "n",
		.desc = "Perform a dry-run - do not make any changes",
		.type = OPTION_FLAG,
		.opt.flag = &cfg.dryrun,
	},
	{
		.name = "v",
		.desc = "Verbose",
		.type = OPTION_FLAG,
		.opt.flag = &cfg.verbose,
	},
	{ NULL },
};

struct hashinfo {
	char *filename;
	char *target;
	unsigned long hash;
	unsigned int index;
	unsigned char fingerprint[EVP_MAX_MD_SIZE];
	int is_crl;
	int is_dup;
	int exists;
	int changed;
	struct hashinfo *reference;
	struct hashinfo *next;
};

static struct hashinfo *
hashinfo(const char *filename, unsigned long hash, unsigned char *fingerprint)
{
	struct hashinfo *hi;

	if ((hi = calloc(1, sizeof(*hi))) == NULL)
		return (NULL);
	if (filename != NULL) {
		if ((hi->filename = strdup(filename)) == NULL) {
			free(hi);
			return (NULL);
		}
	}
	hi->hash = hash;
	if (fingerprint != NULL)
		memcpy(hi->fingerprint, fingerprint, sizeof(hi->fingerprint));

	return (hi);
}

static void
hashinfo_free(struct hashinfo *hi)
{
	if (hi == NULL)
		return;

	free(hi->filename);
	free(hi->target);
	free(hi);
}

#ifdef DEBUG
static void
hashinfo_print(struct hashinfo *hi)
{
	int i;

	printf("hashinfo %s %08lx %u %i\n", hi->filename, hi->hash,
	    hi->index, hi->is_crl);
	for (i = 0; i < (int)EVP_MAX_MD_SIZE; i++) {
		printf("%02X%c", hi->fingerprint[i],
		    (i + 1 == (int)EVP_MAX_MD_SIZE) ? '\n' : ':');
	}
}
#endif

static int
hashinfo_compare(const void *a, const void *b)
{
	struct hashinfo *hia = *(struct hashinfo **)a;
	struct hashinfo *hib = *(struct hashinfo **)b;
	int rv;

	rv = hia->hash < hib->hash ? -1 : hia->hash > hib->hash;
	if (rv != 0)
		return (rv);
	rv = memcmp(hia->fingerprint, hib->fingerprint,
	    sizeof(hia->fingerprint));
	if (rv != 0)
		return (rv);
	return strcmp(hia->filename, hib->filename);
}

static struct hashinfo *
hashinfo_chain(struct hashinfo *head, struct hashinfo *entry)
{
	struct hashinfo *hi = head;

	if (hi == NULL)
		return (entry);
	while (hi->next != NULL)
		hi = hi->next;
	hi->next = entry;

	return (head);
}

static void
hashinfo_chain_free(struct hashinfo *hi)
{
	struct hashinfo *next;

	while (hi != NULL) {
		next = hi->next;
		hashinfo_free(hi);
		hi = next;
	}
}

static size_t
hashinfo_chain_length(struct hashinfo *hi)
{
	int len = 0;

	while (hi != NULL) {
		len++;
		hi = hi->next;
	}
	return (len);
}

static int
hashinfo_chain_sort(struct hashinfo **head)
{
	struct hashinfo **list, *entry;
	size_t len;
	int i;

	if (*head == NULL)
		return (0);

	len = hashinfo_chain_length(*head);
	if ((list = reallocarray(NULL, len, sizeof(struct hashinfo *))) == NULL)
		return (-1);

	for (entry = *head, i = 0; entry != NULL; entry = entry->next, i++)
		list[i] = entry;
	qsort(list, len, sizeof(struct hashinfo *), hashinfo_compare);

	*head = entry = list[0];
	for (i = 1; i < len; i++) {
		entry->next = list[i];
		entry = list[i];
	}
	entry->next = NULL;

	free(list);
	return (0);
}

static char *
hashinfo_linkname(struct hashinfo *hi)
{
	char *filename;

	if (asprintf(&filename, "%08lx.%s%u", hi->hash,
	    (hi->is_crl ? "r" : ""), hi->index) == -1)
		return (NULL);

	return (filename);
}

static int
filename_is_hash(const char *filename)
{
	const char *p = filename;

	while ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f'))
		p++;
	if (*p++ != '.')
		return (0);
	if (*p == 'r')		/* CRL format. */
		p++;
	while (*p >= '0' && *p <= '9')
		p++;
	if (*p != '\0')
		return (0);

	return (1);
}

static int
filename_is_pem(const char *filename)
{
	const char *q, *p = filename;

	if ((q = strchr(p, '\0')) == NULL)
		return (0);
	if ((q - p) < 4)
		return (0);
	if (strncmp((q - 4), ".pem", 4) != 0)
		return (0);

	return (1);
}

static struct hashinfo *
hashinfo_from_linkname(const char *linkname, const char *target)
{
	struct hashinfo *hi = NULL;
	const char *errstr;
	char *l, *p, *ep;
	long long val;

	if ((l = strdup(linkname)) == NULL)
		goto err;
	if ((p = strchr(l, '.')) == NULL)
		goto err;
	*p++ = '\0';

	if ((hi = hashinfo(linkname, 0, NULL)) == NULL)
		goto err;
	if ((hi->target = strdup(target)) == NULL)
		goto err;

	errno = 0;
	val = strtoll(l, &ep, 16);
	if (l[0] == '\0' || *ep != '\0')
		goto err;
	if (errno == ERANGE && (val == LLONG_MAX || val == LLONG_MIN))
		goto err;
	if (val < 0 || val > ULONG_MAX)
		goto err;
	hi->hash = (unsigned long)val;

	if (*p == 'r') {
		hi->is_crl = 1;
		p++;
	}

	val = strtonum(p, 0, 0xffffffff, &errstr);
	if (errstr != NULL)
		goto err;

	hi->index = (unsigned int)val;

	goto done;

 err:
	hashinfo_free(hi);
	hi = NULL;

 done:
	free(l);

	return (hi);
}

static struct hashinfo *
certhash_cert(BIO *bio, const char *filename)
{
	unsigned char fingerprint[EVP_MAX_MD_SIZE];
	struct hashinfo *hi = NULL;
	const EVP_MD *digest;
	X509 *cert = NULL;
	unsigned long hash;
	unsigned int len;

	if ((cert = PEM_read_bio_X509(bio, NULL, NULL, NULL)) == NULL)
		goto err;

	hash = X509_subject_name_hash(cert);

	digest = EVP_sha256();
	if (X509_digest(cert, digest, fingerprint, &len) != 1) {
		fprintf(stderr, "out of memory\n");
		goto err;
	}

	hi = hashinfo(filename, hash, fingerprint);

 err:
	X509_free(cert);

	return (hi);
}

static struct hashinfo *
certhash_crl(BIO *bio, const char *filename)
{
	unsigned char fingerprint[EVP_MAX_MD_SIZE];
	struct hashinfo *hi = NULL;
	const EVP_MD *digest;
	X509_CRL *crl = NULL;
	unsigned long hash;
	unsigned int len;

	if ((crl = PEM_read_bio_X509_CRL(bio, NULL, NULL, NULL)) == NULL)
		return (NULL);

	hash = X509_NAME_hash(X509_CRL_get_issuer(crl));

	digest = EVP_sha256();
	if (X509_CRL_digest(crl, digest, fingerprint, &len) != 1) {
		fprintf(stderr, "out of memory\n");
		goto err;
	}

	hi = hashinfo(filename, hash, fingerprint);

 err:
	X509_CRL_free(crl);

	return (hi);
}

static int
certhash_addlink(struct hashinfo **links, struct hashinfo *hi)
{
	struct hashinfo *link = NULL;

	if ((link = hashinfo(NULL, hi->hash, hi->fingerprint)) == NULL)
		goto err;

	if ((link->filename = hashinfo_linkname(hi)) == NULL)
		goto err;

	link->reference = hi;
	link->changed = 1;
	*links = hashinfo_chain(*links, link);
	hi->reference = link;

	return (0);

 err:
	hashinfo_free(link);
	return (-1);
}

static void
certhash_findlink(struct hashinfo *links, struct hashinfo *hi)
{
	struct hashinfo *link;

	for (link = links; link != NULL; link = link->next) {
		if (link->is_crl == hi->is_crl &&
		    link->hash == hi->hash &&
		    link->index == hi->index &&
		    link->reference == NULL) {
			link->reference = hi;
			if (link->target == NULL ||
			    strcmp(link->target, hi->filename) != 0)
				link->changed = 1;
			hi->reference = link;
			break;
		}
	}
}

static void
certhash_index(struct hashinfo *head, const char *name)
{
	struct hashinfo *last, *entry;
	int index = 0;

	last = NULL;
	for (entry = head; entry != NULL; entry = entry->next) {
		if (last != NULL) {
			if (entry->hash == last->hash) {
				if (memcmp(entry->fingerprint,
				    last->fingerprint,
				    sizeof(entry->fingerprint)) == 0) {
					fprintf(stderr, "WARNING: duplicate %s "
					    "in %s (using %s), ignoring...\n",
					    name, entry->filename,
					    last->filename);
					entry->is_dup = 1;
					continue;
				}
				index++;
			} else {
				index = 0;
			}
		}
		entry->index = index;
		last = entry;
	}
}

static int
certhash_merge(struct hashinfo **links, struct hashinfo **certs,
    struct hashinfo **crls)
{
	struct hashinfo *cert, *crl;

	/* Pass 1 - sort and index entries. */
	if (hashinfo_chain_sort(certs) == -1)
		return (-1);
	if (hashinfo_chain_sort(crls) == -1)
		return (-1);
	certhash_index(*certs, "certificate");
	certhash_index(*crls, "CRL");

	/* Pass 2 - map to existing links. */
	for (cert = *certs; cert != NULL; cert = cert->next) {
		if (cert->is_dup == 1)
			continue;
		certhash_findlink(*links, cert);
	}
	for (crl = *crls; crl != NULL; crl = crl->next) {
		if (crl->is_dup == 1)
			continue;
		certhash_findlink(*links, crl);
	}

	/* Pass 3 - determine missing links. */
	for (cert = *certs; cert != NULL; cert = cert->next) {
		if (cert->is_dup == 1 || cert->reference != NULL)
			continue;
		if (certhash_addlink(links, cert) == -1)
			return (-1);
	}
	for (crl = *crls; crl != NULL; crl = crl->next) {
		if (crl->is_dup == 1 || crl->reference != NULL)
			continue;
		if (certhash_addlink(links, crl) == -1)
			return (-1);
	}

	return (0);
}

static int
certhash_link(struct dirent *dep, struct hashinfo **links)
{
	struct hashinfo *hi = NULL;
	char target[PATH_MAX];
	struct stat sb;
	int n;

	if (lstat(dep->d_name, &sb) == -1) {
		fprintf(stderr, "failed to stat %s\n", dep->d_name);
		return (-1);
	}
	if (!S_ISLNK(sb.st_mode))
		return (0);

	n = readlink(dep->d_name, target, sizeof(target) - 1);
	if (n == -1) {
		fprintf(stderr, "failed to readlink %s\n", dep->d_name);
		return (-1);
	}
	if (n >= sizeof(target) - 1) {
		fprintf(stderr, "symbolic link is too long %s\n", dep->d_name);
		return (-1);
	}
	target[n] = '\0';

	hi = hashinfo_from_linkname(dep->d_name, target);
	if (hi == NULL) {
		fprintf(stderr, "failed to get hash info %s\n", dep->d_name);
		return (-1);
	}
	hi->exists = 1;
	*links = hashinfo_chain(*links, hi);

	return (0);
}

static int
certhash_file(struct dirent *dep, struct hashinfo **certs,
    struct hashinfo **crls)
{
	struct hashinfo *hi = NULL;
	int has_cert, has_crl;
	int ret = -1;
	BIO *bio = NULL;
	FILE *f;

	has_cert = has_crl = 0;

	if ((f = fopen(dep->d_name, "r")) == NULL) {
		fprintf(stderr, "failed to fopen %s\n", dep->d_name);
		goto err;
	}
	if ((bio = BIO_new_fp(f, BIO_CLOSE)) == NULL) {
		fprintf(stderr, "failed to create bio\n");
		fclose(f);
		goto err;
	}

	if ((hi = certhash_cert(bio, dep->d_name)) != NULL) {
		has_cert = 1;
		*certs = hashinfo_chain(*certs, hi);
	}

	if (BIO_reset(bio) != 0) {
		fprintf(stderr, "BIO_reset failed\n");
		goto err;
	}

	if ((hi = certhash_crl(bio, dep->d_name)) != NULL) {
		has_crl = hi->is_crl = 1;
		*crls = hashinfo_chain(*crls, hi);
	}

	if (!has_cert && !has_crl)
		fprintf(stderr, "PEM file %s does not contain a certificate "
		    "or CRL, ignoring...\n", dep->d_name);

	ret = 0;

 err:
	BIO_free(bio);

	return (ret);
}

static int
certhash_directory(const char *path)
{
	struct hashinfo *links = NULL, *certs = NULL, *crls = NULL, *link;
	int ret = 0;
	struct dirent *dep;
	DIR *dip = NULL;

	if ((dip = opendir(".")) == NULL) {
		fprintf(stderr, "failed to open directory %s\n", path);
		goto err;
	}

	if (cfg.verbose)
		fprintf(stdout, "scanning directory %s\n", path);

	/* Create lists of existing hash links, certs and CRLs. */
	while ((dep = readdir(dip)) != NULL) {
		if (filename_is_hash(dep->d_name)) {
			if (certhash_link(dep, &links) == -1)
				goto err;
		}
		if (filename_is_pem(dep->d_name)) {
			if (certhash_file(dep, &certs, &crls) == -1)
				goto err;
		}
	}

	if (certhash_merge(&links, &certs, &crls) == -1) {
		fprintf(stderr, "certhash merge failed\n");
		goto err;
	}

	/* Remove spurious links. */
	for (link = links; link != NULL; link = link->next) {
		if (link->exists == 0 ||
		    (link->reference != NULL && link->changed == 0))
			continue;
		if (cfg.verbose)
			fprintf(stdout, "%s link %s -> %s\n",
			    (cfg.dryrun ? "would remove" :
				"removing"), link->filename, link->target);
		if (cfg.dryrun)
			continue;
		if (unlink(link->filename) == -1) {
			fprintf(stderr, "failed to remove link %s\n",
			    link->filename);
			goto err;
		}
	}

	/* Create missing links. */
	for (link = links; link != NULL; link = link->next) {
		if (link->exists == 1 && link->changed == 0)
			continue;
		if (cfg.verbose)
			fprintf(stdout, "%s link %s -> %s\n",
			    (cfg.dryrun ? "would create" :
				"creating"), link->filename,
			    link->reference->filename);
		if (cfg.dryrun)
			continue;
		if (symlink(link->reference->filename, link->filename) == -1) {
			fprintf(stderr, "failed to create link %s -> %s\n",
			    link->filename, link->reference->filename);
			goto err;
		}
	}

	goto done;

 err:
	ret = 1;

 done:
	hashinfo_chain_free(certs);
	hashinfo_chain_free(crls);
	hashinfo_chain_free(links);

	if (dip != NULL)
		closedir(dip);
	return (ret);
}

static void
certhash_usage(void)
{
	fprintf(stderr, "usage: certhash [-nv] dir ...\n");
	options_usage(certhash_options);
}

int
certhash_main(int argc, char **argv)
{
	int argsused;
	int i, cwdfd, ret = 0;

	if (pledge("stdio cpath wpath rpath", NULL) == -1) {
		perror("pledge");
		exit(1);
	}

	memset(&cfg, 0, sizeof(cfg));

	if (options_parse(argc, argv, certhash_options, NULL, &argsused) != 0) {
                certhash_usage();
                return (1);
        }

	if ((cwdfd = open(".", O_RDONLY)) == -1) {
		perror("failed to open current directory");
		return (1);
	}

	for (i = argsused; i < argc; i++) {
		if (chdir(argv[i]) == -1) {
			fprintf(stderr,
			    "failed to change to directory %s: %s\n",
			    argv[i], strerror(errno));
			ret = 1;
			continue;
		}
		ret |= certhash_directory(argv[i]);
		if (fchdir(cwdfd) == -1) {
			perror("failed to restore current directory");
			ret = 1;
			break;		/* can't continue safely */
		}
	}
	close(cwdfd);

	return (ret);
}