diff options
Diffstat (limited to 'update-ca.c')
-rw-r--r-- | update-ca.c | 362 |
1 files changed, 362 insertions, 0 deletions
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; +} |