From d367723c4747ad369c8ce7f5a64c8a4c37e5f5c3 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Fri, 28 Aug 2020 17:45:42 -0400 Subject: Refactor certificate loading and load all certs from config paths --- Makefile | 1 + README.7 | 4 +- bounce.c | 145 +++++++++++++++++++++++++++------------------------------------ bounce.h | 9 ++++ cert.c | 95 +++++++++++++++++++++++++++++++++++++++++ pounce.1 | 36 ++++++++-------- 6 files changed, 187 insertions(+), 103 deletions(-) create mode 100644 cert.c diff --git a/Makefile b/Makefile index 2eb2491..75b020d 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ MANS = ${BINS:=.1} -include config.mk OBJS += bounce.o +OBJS += cert.o OBJS += client.o OBJS += config.o OBJS += local.o diff --git a/README.7 b/README.7 index 77ba236..0d6bc16 100644 --- a/README.7 +++ b/README.7 @@ -1,4 +1,4 @@ -.Dd August 27, 2020 +.Dd August 28, 2020 .Dt README 7 .Os "Causal Agency" . @@ -131,6 +131,8 @@ remote client connections state shared between clients .It Pa ring.c buffer between server and clients +.It Pa cert.c +sandboxed certificate reloading .It Pa config.c .Xr getopt_long 3 Ns -integrated configuration parsing diff --git a/bounce.c b/bounce.c index 1ef3890..67b5f99 100644 --- a/bounce.c +++ b/bounce.c @@ -177,77 +177,11 @@ static void saveLoad(const char *path) { atexit(saveSave); } -struct SplitPath { - int dir; - char *file; - int targetDir; -}; - -static bool linkTarget(char *target, size_t cap, int dir, const char *file) { - ssize_t len = readlinkat(dir, file, target, cap - 1); - if (len < 0 && errno == EINVAL) return false; - if (len < 0) err(EX_NOINPUT, "%s", file); - target[len] = '\0'; - return true; -} - -static struct SplitPath splitPath(char *path) { - struct SplitPath split = { .targetDir = -1 }; - split.file = strrchr(path, '/'); - if (split.file) { - *split.file++ = '\0'; - split.dir = open(path, O_DIRECTORY); - } else { - split.file = path; - split.dir = open(".", O_DIRECTORY); - } - if (split.dir < 0) err(EX_NOINPUT, "%s", path); - - // Capsicum workaround for certbot "live" symlinks to "../../archive". - char target[PATH_MAX]; - if (!linkTarget(target, sizeof(target), split.dir, split.file)) { - return split; - } - char *file = strrchr(target, '/'); - if (file) { - *file = '\0'; - split.targetDir = openat(split.dir, target, O_DIRECTORY); - if (split.targetDir < 0) err(EX_NOINPUT, "%s", target); - } - - return split; -} - -static FILE *splitOpen(struct SplitPath split) { - if (split.targetDir >= 0) { - char target[PATH_MAX]; - if (!linkTarget(target, sizeof(target), split.dir, split.file)) { - errx(EX_CONFIG, "file is no longer a symlink"); - } - split.dir = split.targetDir; - split.file = strrchr(target, '/'); - if (!split.file) { - errx(EX_CONFIG, "symlink no longer targets directory"); - } - split.file++; - } - - int fd = openat(split.dir, split.file, O_RDONLY); - if (fd < 0) err(EX_NOINPUT, "%s", split.file); - FILE *file = fdopen(fd, "r"); - if (!file) err(EX_IOERR, "fdopen"); - return file; -} - #ifdef __FreeBSD__ static void capLimit(int fd, const cap_rights_t *rights) { int error = cap_rights_limit(fd, rights); if (error) err(EX_OSERR, "cap_rights_limit"); } -static void capLimitSplit(struct SplitPath split, const cap_rights_t *rights) { - capLimit(split.dir, rights); - if (split.targetDir >= 0) capLimit(split.targetDir, rights); -} #endif static volatile sig_atomic_t signals[NSIG]; @@ -437,19 +371,43 @@ int main(int argc, char *argv[]) { ringAlloc(ringSize); if (savePath) saveLoad(savePath); - FILE *localCA = NULL; + struct Cert localCA = { -1, -1, "" }; if (caPath) { - localCA = configOpen(caPath, "r"); - if (!localCA) return EX_NOINPUT; + const char *dirs = NULL; + for (const char *path; NULL != (path = configPath(&dirs, caPath));) { + error = certOpen(&localCA, path); + if (!error) break; + } + if (error) err(EX_NOINPUT, "%s", caPath); } - struct SplitPath certSplit = splitPath(certPath); - struct SplitPath privSplit = splitPath(privPath); - FILE *cert = splitOpen(certSplit); - FILE *priv = splitOpen(privSplit); - localConfig(cert, priv, localCA, !clientPass); - fclose(cert); - fclose(priv); + const char *dirs; + struct Cert cert; + struct Cert priv; + dirs = NULL; + for (const char *path; NULL != (path = configPath(&dirs, certPath));) { + error = certOpen(&cert, path); + if (!error) break; + } + if (error) err(EX_NOINPUT, "%s", certPath); + dirs = NULL; + for (const char *path; NULL != (path = configPath(&dirs, privPath));) { + error = certOpen(&priv, path); + if (!error) break; + } + if (error) err(EX_NOINPUT, "%s", privPath); + + FILE *certRead = certFile(&cert); + if (!certRead) err(EX_NOINPUT, "%s", certPath); + FILE *privRead = certFile(&priv); + if (!privRead) err(EX_NOINPUT, "%s", privPath); + FILE *caRead = (caPath ? certFile(&localCA) : NULL); + if (caPath && !caRead) err(EX_NOINPUT, "%s", caPath); + + localConfig(certRead, privRead, caRead, !clientPass); + fclose(certRead); + fclose(privRead); + if (caPath) fclose(caRead); int bind[8]; size_t binds = bindPath[0] @@ -471,9 +429,14 @@ int main(int argc, char *argv[]) { cap_rights_merge(&bindRights, &sockRights); if (saveFile) capLimit(fileno(saveFile), &saveRights); - if (localCA) capLimit(fileno(localCA), &fileRights); - capLimitSplit(certSplit, &fileRights); - capLimitSplit(privSplit, &fileRights); + capLimit(cert.parent, &fileRights); + capLimit(cert.target, &fileRights); + capLimit(priv.parent, &fileRights); + capLimit(priv.target, &fileRights); + if (caPath) { + capLimit(localCA.parent, &fileRights); + capLimit(localCA.target, &fileRights); + } for (size_t i = 0; i < binds; ++i) { capLimit(bind[i], &bindRights); } @@ -566,11 +529,25 @@ int main(int argc, char *argv[]) { if (signals[SIGUSR1]) { signals[SIGUSR1] = 0; - cert = splitOpen(certSplit); - priv = splitOpen(privSplit); - localConfig(cert, priv, localCA, !clientPass); - fclose(cert); - fclose(priv); + certRead = certFile(&cert); + if (!certRead) { + warn("%s", certPath); + continue; + } + privRead = certFile(&priv); + if (!privRead) { + warn("%s", privPath); + continue; + } + caRead = (caPath ? certFile(&localCA) : NULL); + if (caPath && !caRead) { + warn("%s", caPath); + continue; + } + localConfig(certRead, privRead, caRead, !clientPass); + fclose(certRead); + fclose(privRead); + if (caPath) fclose(caRead); } } diff --git a/bounce.h b/bounce.h index b09a349..6b376ae 100644 --- a/bounce.h +++ b/bounce.h @@ -25,6 +25,7 @@ * covered work. */ +#include #include #include #include @@ -208,6 +209,14 @@ void stateSync(struct Client *client); const char *stateNick(void); const char *stateEcho(void); +struct Cert { + int parent; + int target; + char name[NAME_MAX]; +}; +int certOpen(struct Cert *cert, const char *path); +FILE *certFile(const struct Cert *cert); + const char *configPath(const char **dirs, const char *path); const char *dataPath(const char **dirs, const char *path); FILE *configOpen(const char *path, const char *mode); diff --git a/cert.c b/cert.c new file mode 100644 index 0000000..23c9ce8 --- /dev/null +++ b/cert.c @@ -0,0 +1,95 @@ +/* Copyright (C) 2020 C. McEnroe + * + * 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 3 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, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with OpenSSL (or a modified version of that library), + * containing parts covered by the terms of the OpenSSL License and the + * original SSLeay license, the licensors of this Program grant you + * additional permission to convey the resulting work. Corresponding + * Source for a non-source form of such a combination shall include the + * source code for the parts of OpenSSL used as well as that of the + * covered work. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bounce.h" + +// This basically exists to work around certbot's symlinks from "live" into +// "archive" under capsicum. + +int certOpen(struct Cert *cert, const char *path) { + char buf[PATH_MAX]; + snprintf(buf, sizeof(buf), "%s", path); + + char *base = strrchr(buf, '/'); + if (base) { + *base = '\0'; + snprintf(cert->name, sizeof(cert->name), "%s", &base[1]); + cert->parent = open(buf, O_DIRECTORY); + } else { + snprintf(cert->name, sizeof(cert->name), "%s", path); + cert->parent = open(".", O_DIRECTORY); + } + if (cert->parent < 0) return -1; + + cert->target = cert->parent; + ssize_t len = readlinkat(cert->parent, cert->name, buf, sizeof(buf) - 1); + if (len < 0 && errno == EINVAL) return 0; + if (len < 0) return -1; + buf[len] = '\0'; + + base = strrchr(buf, '/'); + if (base) { + *base = '\0'; + cert->target = openat(cert->parent, buf, O_DIRECTORY); + if (cert->target < 0) return -1; + } + return 0; +} + +FILE *certFile(const struct Cert *cert) { + const char *name = cert->name; + + char buf[PATH_MAX]; + ssize_t len = readlinkat(cert->parent, cert->name, buf, sizeof(buf) - 1); + if (len < 0) { + if (errno != EINVAL) return NULL; + } else { + // XXX: Assume only the target base name has changed. + buf[len] = '\0'; + name = strrchr(buf, '/'); + if (name) { + name = &name[1]; + } else { + name = buf; + } + } + + int fd = openat(cert->target, name, O_RDONLY); + if (fd < 0) return NULL; + + return fdopen(fd, "r"); +} diff --git a/pounce.1 b/pounce.1 index f0ba78b..fa2cb64 100644 --- a/pounce.1 +++ b/pounce.1 @@ -1,4 +1,4 @@ -.Dd August 27, 2020 +.Dd August 28, 2020 .Dt POUNCE 1 .Os . @@ -96,6 +96,8 @@ unless the path starts with .Ql / or .Ql \&. . +Certificate and private key paths +are searched for in the same manner. Each option is placed on a line, and lines beginning with .Ql # @@ -111,9 +113,7 @@ The arguments are as follows: Require clients to authenticate using a TLS client certificate signed by the certificate authority loaded from -.Ar path , -which is searched for -in the same manner as configuration files. +.Ar path . See .Sx Generating Client Certificates . If @@ -241,9 +241,7 @@ it is recommended to use SASL EXTERNAL instead with . .It Fl c Ar path , Cm client-cert = Ar path Load the TLS client certificate from -.Ar path , -which is searched for -in the same manner as configuration files. +.Ar path . If the private key is in a separate file, it is loaded with .Fl k . @@ -295,9 +293,7 @@ Join the comma-separated list of . .It Fl k Ar path , Cm client-priv = Ar path Load the TLS client private key from -.Ar path , -which is searched for -in the same manner as configuration files. +.Ar path . . .It Fl n Ar nick , Cm nick = Ar nick Set nickname to @@ -379,12 +375,13 @@ daemon exits. Upon receiving the .Dv SIGUSR1 signal, -the certificate and private key +the certificate, private key and local CA will be reloaded from the paths specified by -.Fl C +.Fl C , +.Fl K and -.Fl K . +.Fl A . . .Ss Client Configuration Clients should be configured to @@ -460,8 +457,8 @@ pounce -g client2.pem .It Concatenate the certificate public keys into a CA file: .Bd -literal -offset indent -openssl x509 -subject -in client1.pem >> auth.pem -openssl x509 -subject -in client2.pem >> auth.pem +openssl x509 -subject -in client1.pem >> ~/.config/pounce/auth.pem +openssl x509 -subject -in client2.pem >> ~/.config/pounce/auth.pem .Ed .It Configure @@ -497,7 +494,7 @@ Since only the public key is needed for certificate verification, extract it from the CA: .Bd -literal -offset indent -openssl x509 -in auth.pem -out auth.crt +openssl x509 -in auth.pem -out ~/.config/pounce/auth.crt .Ed .It Configure @@ -515,7 +512,7 @@ local-ca = auth.crt .It Generate a new TLS client certificate: .Bd -literal -offset indent -pounce -g example.pem +pounce -g ~/.config/pounce/example.pem .Ed .It Connect to the server using the certificate: @@ -549,7 +546,8 @@ The default nickname. .Sh FILES .Bl -tag -width Ds .It Pa $XDG_CONFIG_DIRS/pounce -Configuration files are searched for first in +Configuration files, certificates and private keys +are searched for first in .Ev $XDG_CONFIG_HOME , usually .Pa ~/.config , @@ -569,6 +567,8 @@ followed by the colon-separated list of paths .Ev $XDG_DATA_DIRS , usually .Pa /usr/local/share:/usr/share . +New save files are created in +.Ev $XDG_DATA_HOME . .It Pa ~/.local/share/pounce The most likely location of save files. .El -- cgit 1.4.1