about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJune McEnroe <june@causal.agency>2020-08-28 17:45:42 -0400
committerJune McEnroe <june@causal.agency>2020-08-28 18:14:25 -0400
commitd367723c4747ad369c8ce7f5a64c8a4c37e5f5c3 (patch)
tree3d526646ddb2adafa10bdd6ace8f231a09df154d
parentSandbox pounce with pledge(2) (diff)
downloadpounce-d367723c4747ad369c8ce7f5a64c8a4c37e5f5c3.tar.gz
pounce-d367723c4747ad369c8ce7f5a64c8a4c37e5f5c3.zip
Refactor certificate loading and load all certs from config paths
-rw-r--r--Makefile1
-rw-r--r--README.74
-rw-r--r--bounce.c145
-rw-r--r--bounce.h9
-rw-r--r--cert.c95
-rw-r--r--pounce.136
6 files changed, 187 insertions, 103 deletions
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 <limits.h>
 #include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -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 <june@causal.agency>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * 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 <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+#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