#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;
}
static bool dir_readfiles(struct hash* d, const char* path,
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) {
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, &proc_localglobaldir, fd);
/* Update etc cert dir for additions and deletions*/
dir_readfiles(calinks, ETCCERTSDIR, &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 };
execv("/usr/bin/run-parts", run_parts_args);
execv("/bin/run-parts", run_parts_args);
perror("run-parts");
return 1;
}