diff options
-rw-r--r-- | c_rehash.c | 375 | ||||
-rw-r--r-- | certdata2pem.py | 155 | ||||
-rw-r--r-- | update-ca-certificates.8 | 73 | ||||
-rw-r--r-- | update-ca.c | 362 |
4 files changed, 965 insertions, 0 deletions
diff --git a/c_rehash.c b/c_rehash.c new file mode 100644 index 0000000..5575492 --- /dev/null +++ b/c_rehash.c @@ -0,0 +1,375 @@ +/* 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 <timo.teras@iki.fi> + * All rights reserved. + * + * This software is licensed under the MIT License. + * Full license available at: http://opensource.org/licenses/MIT + */ + +#include <stdio.h> +#include <limits.h> +#include <string.h> +#include <unistd.h> +#include <dirent.h> +#include <sys/stat.h> + +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/x509.h> + +#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 <args> <dirs>\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; +} diff --git a/certdata2pem.py b/certdata2pem.py new file mode 100644 index 0000000..f91422b --- /dev/null +++ b/certdata2pem.py @@ -0,0 +1,155 @@ +#!/usr/bin/python +# vim:set et sw=4: +# +# certdata2pem.py - splits certdata.txt into multiple files +# +# Copyright (C) 2009 Philipp Kern <pkern@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, +# USA. + +import base64 +import os.path +import re +import sys +import textwrap +import io + +objects = [] + +# Dirty file parser. +in_data, in_multiline, in_obj = False, False, False +field, type, value, obj = None, None, None, dict() + +# Python 3 will not let us decode non-ascii characters if we +# have not specified an encoding, but Python 2's open does not +# have an option to set the encoding. Python 3's open is io.open +# and io.open has been backported to Python 2.6 and 2.7, so use io.open. +for line in io.open('certdata.txt', 'rt', encoding='utf8'): + # Ignore the file header. + if not in_data: + if line.startswith('BEGINDATA'): + in_data = True + continue + # Ignore comment lines. + if line.startswith('#'): + continue + # Empty lines are significant if we are inside an object. + if in_obj and len(line.strip()) == 0: + objects.append(obj) + obj = dict() + in_obj = False + continue + if len(line.strip()) == 0: + continue + if in_multiline: + if not line.startswith('END'): + if type == 'MULTILINE_OCTAL': + line = line.strip() + for i in re.finditer(r'\\([0-3][0-7][0-7])', line): + value.append(int(i.group(1), 8)) + else: + value += line + continue + obj[field] = value + in_multiline = False + continue + if line.startswith('CKA_CLASS'): + in_obj = True + line_parts = line.strip().split(' ', 2) + if len(line_parts) > 2: + field, type = line_parts[0:2] + value = ' '.join(line_parts[2:]) + elif len(line_parts) == 2: + field, type = line_parts + value = None + else: + raise NotImplementedError('line_parts < 2 not supported.') + if type == 'MULTILINE_OCTAL': + in_multiline = True + value = bytearray() + continue + obj[field] = value +if len(obj) > 0: + objects.append(obj) + +# Read blacklist. +blacklist = [] +if os.path.exists('blacklist.txt'): + for line in open('blacklist.txt', 'r'): + line = line.strip() + if line.startswith('#') or len(line) == 0: + continue + item = line.split('#', 1)[0].strip() + blacklist.append(item) + +# Build up trust database. +trust = dict() +for obj in objects: + if obj['CKA_CLASS'] != 'CKO_NSS_TRUST': + continue + if obj['CKA_LABEL'] in blacklist: + print("Certificate %s blacklisted, ignoring." % obj['CKA_LABEL']) + elif obj['CKA_TRUST_SERVER_AUTH'] == 'CKT_NSS_TRUSTED_DELEGATOR': + trust[obj['CKA_LABEL']] = True + elif obj['CKA_TRUST_EMAIL_PROTECTION'] == 'CKT_NSS_TRUSTED_DELEGATOR': + trust[obj['CKA_LABEL']] = True + elif obj['CKA_TRUST_SERVER_AUTH'] == 'CKT_NSS_NOT_TRUSTED': + print('!'*74) + print("UNTRUSTED BUT NOT BLACKLISTED CERTIFICATE FOUND: %s" % obj['CKA_LABEL']) + print('!'*74) + else: + print("Ignoring certificate %s. SAUTH=%s, EPROT=%s" % \ + (obj['CKA_LABEL'], obj['CKA_TRUST_SERVER_AUTH'], + obj['CKA_TRUST_EMAIL_PROTECTION'])) + +for obj in objects: + if obj['CKA_CLASS'] == 'CKO_CERTIFICATE': + if not obj['CKA_LABEL'] in trust or not trust[obj['CKA_LABEL']]: + continue + bname = obj['CKA_LABEL'][1:-1].replace('/', '_')\ + .replace(' ', '_')\ + .replace('(', '=')\ + .replace(')', '=')\ + .replace(',', '_') + + # this is the only way to decode the way NSS stores multi-byte UTF-8 + # and we need an escaped string for checking existence of things + # otherwise we're dependant on the user's current locale. + if bytes != str: + # We're in python 3, convert the utf-8 string to a + # sequence of bytes that represents this utf-8 string + # then encode the byte-sequence as an escaped string that + # can be passed to open() and os.path.exists() + bname = bname.encode('utf-8').decode('unicode_escape').encode('latin-1') + else: + # Python 2 + # Convert the unicode string back to its original byte form + # (contents of files returned by io.open are returned as + # unicode strings) + # then to an escaped string that can be passed to open() + # and os.path.exists() + bname = bname.encode('utf-8').decode('string_escape') + + fname = bname + b'.crt' + if os.path.exists(fname): + print("Found duplicate certificate name %s, renaming." % bname) + fname = bname + b'_2.crt' + f = open(fname, 'w') + f.write("-----BEGIN CERTIFICATE-----\n") + encoded = base64.b64encode(obj['CKA_VALUE']).decode('utf-8') + f.write("\n".join(textwrap.wrap(encoded, 64))) + f.write("\n-----END CERTIFICATE-----\n") + diff --git a/update-ca-certificates.8 b/update-ca-certificates.8 new file mode 100644 index 0000000..b149b1d --- /dev/null +++ b/update-ca-certificates.8 @@ -0,0 +1,73 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" First parameter, NAME, should be all caps +.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection +.\" other parameters are allowed: see man(7), man(1) +.TH UPDATE-CA-CERTIFICATES 8 "20 April 2003" +.\" Please adjust this date whenever revising the manpage. +.\" +.\" Some roff macros, for reference: +.\" .nh disable hyphenation +.\" .hy enable hyphenation +.\" .ad l left justify +.\" .ad b justify to both left and right margins +.\" .nf disable filling +.\" .fi enable filling +.\" .br insert line break +.\" .sp <n> insert n+1 empty lines +.\" for manpage-specific macros, see man(7) +.SH NAME +update-ca-certificates \- update /etc/ssl/certs and ca-certificates.crt +.SH SYNOPSIS +.B update-ca-certificates +.RI [ options ] +.SH DESCRIPTION +This manual page documents briefly the +.B update-ca-certificates +command. +.PP +\fBupdate-ca-certificates\fP is a program that updates the directory +/etc/ssl/certs to hold SSL certificates and generates ca-certificates.crt, +a concatenated single-file list of certificates. +.PP +It reads the file /etc/ca-certificates.conf. Each line gives a pathname of +a CA certificate under /usr/share/ca-certificates that should be trusted. +Lines that begin with "#" are comment lines and thus ignored. +Lines that begin with "!" are deselected, causing the deactivation of the CA +certificate in question. Certificates must have a .crt extension in order to +be included by update-ca-certificates. +.PP +Furthermore all certificates with a .crt extension found below +/usr/local/share/ca-certificates are also included as implicitly trusted. +.PP +Before terminating, \fBupdate-ca-certificates\fP invokes +\fBrun-parts\fP on /etc/ca-certificates/update.d. +.SH OPTIONS +A summary of options is included below. +.TP +.B \-h, \-\-help +Show summary of options. +.TP +.B \-v, \-\-verbose +Be verbose. Output \fBc_rehash\fP. +.TP +.B \-f, \-\-fresh +Fresh updates. Remove symlinks in /etc/ssl/certs directory. +.SH FILES +.TP +.I /etc/ca-certificates.conf +A configuration file. +.TP +.I /etc/ssl/certs/ca-certificates.crt +A single-file version of CA certificates. This holds +all CA certificates that you activated in /etc/ca-certificates.conf. +.TP +.I /usr/share/ca-certificates +Directory of CA certificates. +.TP +.I /usr/local/share/ca-certificates +Directory of local CA certificates (with .crt extension). +.SH SEE ALSO +.BR c_rehash (1) +.SH AUTHOR +This manual page was written by Fumitoshi UKAI <ukai@debian.or.jp>, +for the Debian project (but may be used by others). diff --git a/update-ca.c b/update-ca.c new file mode 100644 index 0000000..10ebd02 --- /dev/null +++ b/update-ca.c @@ -0,0 +1,362 @@ +#define _GNU_SOURCE +#include <stdio.h> +#include <stdbool.h> +#include <string.h> +#include <stdlib.h> +#include <dirent.h> +#include <unistd.h> +#include <fcntl.h> +#include <errno.h> +#include <limits.h> + +#include <sys/stat.h> +#include <sys/sendfile.h> + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +#define CERTSDIR "/usr/share/ca-certificates/" +#define LOCALCERTSDIR "/usr/local/share/ca-certificates/" +#define ETCCERTSDIR "/etc/ssl/certs/" +#define RUNPARTSDIR "/etc/ca-certificates/update.d/" +#define CERTBUNDLE "ca-certificates.crt" +#define CERTSCONF "/etc/ca-certificates.conf" + +static const char *last_component(const char *path) +{ + const char *c = strrchr(path, '/'); + if (c) return c + 1; + return path; +} +static bool str_begins(const char* str, const char* prefix) +{ + return !strncmp(str, prefix, strlen(prefix)); +} + +struct hash_item { + struct hash_item *next; + char *key; + char *value; +}; + +struct hash { + struct hash_item *items[256]; +}; + +static unsigned int hash_string(const char *str) +{ + unsigned long h = 5381; + for (; *str; str++) + h = (h << 5) + h + *str; + return h; +} + +static void hash_init(struct hash *h) +{ + memset(h, 0, sizeof *h); +} + +static struct hash_item *hash_get(struct hash *h, const char *key) +{ + unsigned int bucket = hash_string(key) % ARRAY_SIZE(h->items); + struct hash_item *item; + + for (item = h->items[bucket]; item; item = item->next) + if (strcmp(item->key, key) == 0) + return item; + return NULL; +} + +static void hash_foreach(struct hash *h, void (*cb)(struct hash_item *)) +{ + struct hash_item *item; + int i; + + for (i = 0; i < ARRAY_SIZE(h->items); i++) { + for (item = h->items[i]; item; item = item->next) + cb(item); + } +} + +static bool hash_add(struct hash *h, const char *key, const char *value) +{ + unsigned int bucket = hash_string(key) % ARRAY_SIZE(h->items); + size_t keylen = strlen(key), valuelen = strlen(value); + struct hash_item *i; + + i = malloc(sizeof(struct hash_item) + keylen + 1 + valuelen + 1); + if (!i) + return false; + + i->key = (char*)(i+1); + strcpy(i->key, key); + i->value = i->key + keylen + 1; + strcpy(i->value, value); + + i->next = h->items[bucket]; + h->items[bucket] = i; + return true; +} + +static ssize_t +buffered_copyfd(int in_fd, int out_fd, ssize_t in_size) +{ + const size_t bufsize = 8192; + char *buf = NULL; + ssize_t r = 0, w = 0, copied = 0, n, m; + if ((buf = malloc(bufsize)) == NULL) + return -1; + + while (r < in_size && (n = read(in_fd, buf, bufsize))) { + if (n == -1) { + if (errno == EINTR) + continue; + break; + } + r = n; + w = 0; + while (w < r && (n = write(out_fd, buf + w, (r - w)))) { + if (n == -1) { + if (errno == EINTR) + continue; + break; + } + w += n; + } + copied += w; + } + free(buf); + return copied; +} + +static bool +copyfile(const char* source, int output) +{ + off_t bytes = 0; + struct stat fileinfo = {0}; + ssize_t result; + int in_fd; + + if ((in_fd = open(source, O_RDONLY)) == -1) + return false; + + if (fstat(in_fd, &fileinfo) < 0) { + close(in_fd); + return false; + } + + result = sendfile(output, in_fd, &bytes, fileinfo.st_size); + if ((result == -1) && (errno == EINVAL || errno == ENOSYS)) + result = buffered_copyfd(in_fd, output, fileinfo.st_size); + + close(in_fd); + return fileinfo.st_size == result; +} + +typedef void (*proc_path)(const char *fullpath, struct hash *, int); + +static void proc_localglobaldir(const char *fullpath, struct hash *h, int tmpfile_fd) +{ + const char *fname = last_component(fullpath); + size_t flen = strlen(fname); + char *s, *actual_file = NULL; + + /* Snip off the .crt suffix */ + if (flen > 4 && strcmp(&fname[flen-4], ".crt") == 0) + flen -= 4; + + if (asprintf(&actual_file, "%s%.*s%s", + "ca-cert-", + flen, fname, + ".pem") == -1) { + fprintf(stderr, "Cannot open path: %s\n", fullpath); + return; + } + + for (s = actual_file; *s; s++) { + switch(*s) { + case ',': + case ' ': + *s = '_'; + break; + case ')': + case '(': + *s = '='; + break; + default: + break; + } + } + + if (!hash_add(h, actual_file, fullpath)) + fprintf(stderr, "Warning! Cannot hash: %s\n", fullpath); + if (!copyfile(fullpath, tmpfile_fd)) + fprintf(stderr, "Warning! Cannot copy to bundle: %s\n", fullpath); + free(actual_file); +} + +static void proc_etccertsdir(const char* fullpath, struct hash* h, int tmpfile_fd) +{ + char linktarget[SYMLINK_MAX]; + ssize_t linklen; + + linklen = readlink(fullpath, linktarget, sizeof(linktarget)-1); + if (linklen < 0) + return; + linktarget[linklen] = 0; + + struct hash_item *item = hash_get(h, last_component(fullpath)); + if (!item) { + /* Symlink exists but is not wanted + * Delete it if it points to 'our' directory + */ + if (str_begins(linktarget, CERTSDIR) || str_begins(linktarget, LOCALCERTSDIR)) + unlink(fullpath); + } else if (strcmp(linktarget, item->value) != 0) { + /* Symlink exists but points wrong */ + unlink(fullpath); + if (symlink(item->value, fullpath) < 0) + fprintf(stderr, "Warning! Cannot update symlink %s -> %s\n", item->value, fullpath); + item->value = 0; + } else { + /* Symlink exists and is ok */ + item->value = 0; + } +} + +static bool read_global_ca_list(const char* file, struct hash* d, int tmpfile_fd) +{ + FILE * fp = fopen(file, "r"); + if (fp == NULL) + return false; + + char * line = NULL; + size_t len = 0; + ssize_t read; + + while ((read = getline(&line, &len, fp)) != -1) { + /* getline returns number of bytes in buffer, and buffer + * contains delimeter if it was found */ + if (read > 0 && line[read-1] == '\n') + line[read-1] = 0; + if (str_begins(line, "#") || str_begins(line, "!")) + continue; + + char* fullpath = 0; + if (asprintf(&fullpath,"%s%s", CERTSDIR, line) != -1) { + proc_localglobaldir(fullpath, d, tmpfile_fd); + free(fullpath); + } + } + + fclose(fp); + free(line); + return true; +} + +typedef enum { + FILE_LINK, + FILE_REGULAR +} filetype; + +static bool is_filetype(const char* path, filetype file_check) +{ + struct stat statbuf; + + if (lstat(path, &statbuf) < 0) + return false; + switch(file_check) { + case FILE_LINK: return S_ISLNK(statbuf.st_mode); + case FILE_REGULAR: return S_ISREG(statbuf.st_mode); + default: break; + } + + return false; +} + +static bool dir_readfiles(struct hash* d, const char* path, + filetype allowed_file_type, + proc_path path_processor, + int tmpfile_fd) +{ + DIR *dp = opendir(path); + if (!dp) + return false; + + struct dirent *dirp; + while ((dirp = readdir(dp)) != NULL) { + if (str_begins(dirp->d_name, ".")) + continue; + + char* fullpath = 0; + if (asprintf(&fullpath, "%s%s", path, dirp->d_name) != -1) { + if (is_filetype(fullpath, allowed_file_type)) + path_processor(fullpath, d, tmpfile_fd); + + free(fullpath); + } + } + + return closedir(dp) == 0; +} + +static void update_ca_symlink(struct hash_item *item) +{ + if (!item->value) + return; + + char* newpath = 0; + bool build_str = asprintf(&newpath, "%s%s", ETCCERTSDIR, item->key); + if (!build_str || symlink(item->value, newpath) == -1) + fprintf(stderr, "Warning! Cannot symlink %s -> %s\n", + item->value, newpath); + free(newpath); +} + +int main(int a, char **v) +{ + struct hash _calinks, *calinks = &_calinks; + + const char* bundle = "bundleXXXXXX"; + char* tmpfile = 0; + if (asprintf(&tmpfile, "%s%s", ETCCERTSDIR, bundle) == -1) + return 1; + + int fd = mkstemp(tmpfile); + if (fd == -1) { + fprintf(stderr, "Failed to open temporary file %s for ca bundle\n", tmpfile); + return 1; + } + fchmod(fd, 0644); + + hash_init(calinks); + + /* Handle global CA certs from config file */ + read_global_ca_list(CERTSCONF, calinks, fd); + + /* Handle local CA certificates */ + dir_readfiles(calinks, LOCALCERTSDIR, FILE_REGULAR, &proc_localglobaldir, fd); + + /* Update etc cert dir for additions and deletions*/ + dir_readfiles(calinks, ETCCERTSDIR, FILE_LINK, &proc_etccertsdir, fd); + hash_foreach(calinks, update_ca_symlink); + + /* Update hashes and the bundle */ + if (fd != -1) { + close(fd); + char* newcertname = 0; + if (asprintf(&newcertname, "%s%s", ETCCERTSDIR, CERTBUNDLE) != -1) { + rename(tmpfile, newcertname); + free(newcertname); + } + } + + free(tmpfile); + + /* Execute run-parts */ + static const char *run_parts_args[] = { "run-parts", RUNPARTSDIR, 0 }; + execve("/usr/bin/run-parts", run_parts_args, NULL); + execve("/bin/run-parts", run_parts_args, NULL); + perror("run-parts"); + + return 1; +} |