Commit Diff


commit - d2734f084af031fafd9d269c6f91b70460772e68
commit + d35e18b31b0e05c6178a6bfa891dd2e2dadf3db1
blob - cc75de3d7d482e933ac57faa09aec2859f9180a8
blob + c33b2d778d3419cdbf24d4f84a62c70d46dd615b
--- certs.c
+++ certs.c
@@ -18,8 +18,17 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+/*
+ * The routines to generate a certificate were derived from acme-client.
+ */
+
 #include "compat.h"
 
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <fcntl.h>
+#include <limits.h>
 #include <string.h>
 #include <unistd.h>
 
@@ -33,12 +42,251 @@
 #include <openssl/x509v3.h>
 
 #include "certs.h"
+#include "fs.h"
+#include "iri.h"
 
+/* client certificate */
+struct ccert {
+	char	*line;	/* fields below points inside here */
+	char	*host;
+	char	*port;
+	char	*path;
+	char	*cert;
+};
+
+static struct cert_store {
+	struct ccert	*certs;
+	size_t		 len;
+	size_t		 cap;
+} cert_store;
+
+char		**identities;
+static size_t	  id_len, id_cap;
+
 /*
  * Default number of bits when creating a new RSA key.
  */
 #define KBITS 4096
+
+static int
+identities_cmp(const void *a, const void *b)
+{
+	return (strcmp(a, b));
+}
+
+static inline int
+push_identity(char *name)
+{
+	void	*t;
+	size_t	 newcap, i;
+
+	for (i = 0; i < id_len; ++i) {
+		if (!strcmp(identities[i], name))
+			return (0);
+	}
+
+	/* id_cap is initilized to 8 in certs_init() */
+	if (id_len >= id_cap - 1) {
+		newcap = id_cap + 8;
+		t = recallocarray(identities, id_cap, newcap,
+		    sizeof(*identities));
+		if (t == NULL)
+			return (-1);
+		identities = t;
+		id_cap = newcap;
+	}
+
+	identities[id_len++] = name;
+	qsort(identities, id_len, sizeof(*identities), identities_cmp);
+
+	return (0);
+}
+
+static int
+certs_cmp(const void *a, const void *b)
+{
+	const struct ccert	*ca = a, *cb = b;
+	int			 r;
+
+	if ((r = strcmp(ca->host, cb->host)) != 0)
+		return (r);
+	if ((r = strcmp(ca->port, cb->port)) != 0)
+		return (r);
+	if ((r = strcmp(ca->path, cb->path)) != 0)
+		return (r);
+	return (strcmp(ca->cert, cb->cert));
+}
+
+static int
+certs_store_add(const char *l)
+{
+	size_t			 newcap;
+	void			*t;
+	char			*line, *host, *port, *path, *cert;
+
+	if ((line = strdup(l)) == NULL)
+		return (-1);
+
+	host = line;
+	while (isspace((unsigned char)*host))
+		++host;
+
+	if (*host == '#') {
+		free(line);
+		return (0);
+	}
+
+	port = host + strcspn(host, " \t");
+	if (*port == '\0')
+		goto err;
+	*port++ = '\0';
+	while (isspace((unsigned char)*port))
+		++port;
+
+	path = port + strcspn(port, " \t");
+	if (*path == '\0')
+		goto err;
+	*path++ = '\0';
+	while (isspace((unsigned char)*path))
+		++path;
+
+	cert = path + strcspn(path, " \t");
+	if (*cert == '\0')
+		goto err;
+	*cert++ = '\0';
+	while (isspace((unsigned char)*cert))
+		++cert;
+
+	if (*cert == '\0')
+		goto err;
+
+	if (cert_store.len == cert_store.cap) {
+		newcap = cert_store.cap + 8;
+		t = reallocarray(cert_store.certs, newcap,
+		    sizeof(*cert_store.certs));
+		if (t == NULL)
+			goto err;
+		cert_store.certs = t;
+		cert_store.cap = newcap;
+	}
 
+	cert_store.certs[cert_store.len].line = line;
+	cert_store.certs[cert_store.len].host = host;
+	cert_store.certs[cert_store.len].port = port;
+	cert_store.certs[cert_store.len].path = path;
+	cert_store.certs[cert_store.len].cert = cert;
+	cert_store.len++;
+
+	return (push_identity(cert));
+
+ err:
+	free(line);
+	return (-1);
+}
+
+int
+certs_init(const char *certfile)
+{
+	FILE	*fp;
+	char	*line = NULL;
+	size_t	 linesize = 0;
+	ssize_t	 linelen;
+
+	id_cap = 8;
+	if ((identities = calloc(id_cap, sizeof(*identities))) == NULL)
+		return (-1);
+
+	if ((fp = fopen(certfile, "r")) == NULL) {
+		if (errno == ENOENT)
+			return (0);
+		return (-1);
+	}
+
+	while ((linelen = getline(&line, &linesize, fp)) != -1) {
+		if (line[linelen - 1] == '\n')
+			line[--linelen] = '\0';
+
+		if (certs_store_add(line) == -1) {
+			fclose(fp);
+			free(line);
+			return (-1);
+		}
+	}
+
+	if (ferror(fp)) {
+		fclose(fp);
+		free(line);
+		return (-1);
+	}
+
+	/*
+	 * Data should already be in order, so mergesort should be
+	 * faster.  If it fails (memory scarcity), fall back to qsort()
+	 * which is in place.
+	 */
+	if (mergesort(cert_store.certs, cert_store.len,
+	    sizeof(*cert_store.certs), certs_cmp) == -1)
+		qsort(cert_store.certs, cert_store.len,
+		    sizeof(*cert_store.certs), certs_cmp);
+
+	fclose(fp);
+	free(line);
+	return (0);
+}
+
+const char *
+ccert(const char *name)
+{
+	size_t		 i;
+
+	for (i = 0; i < id_len; ++i) {
+		if (!strcmp(name, identities[i]))
+			return (identities[i]);
+	}
+
+	return (NULL);
+}
+
+const char *
+cert_for(struct iri *iri)
+{
+	struct ccert	*c;
+	size_t		 i;
+
+	for (i = 0; i < cert_store.len; ++i) {
+		c = &cert_store.certs[i];
+
+		if (!strcmp(c->host, iri->iri_host) &&
+		    !strcmp(c->port, iri->iri_portstr) &&
+		    !strncmp(c->path, iri->iri_path, strlen(c->path)))
+			return (c->cert);
+	}
+
+	return (NULL);
+}
+
+int
+cert_open(const char *cert)
+{
+	char		 path[PATH_MAX];
+	struct stat	 sb;
+	int		 fd;
+
+	strlcpy(path, cert_dir, sizeof(path));
+	strlcat(path, "/", sizeof(path));
+	strlcat(path, cert, sizeof(path));
+
+	if ((fd = open(path, O_RDONLY)) == -1)
+		return (-1);
+
+	if (fstat(fd, &sb) == -1 || !S_ISREG(sb.st_mode)) {
+		close(fd);
+		return (-1);
+	}
+
+	return (fd);
+}
+
 static EVP_PKEY *
 rsa_key_create(FILE *f, const char *fname)
 {
blob - c50f46afdb218208b3c062887aca4274c3d8fec9
blob + 02ed3f170e25607b1fa02036f749359c51db5996
--- certs.h
+++ certs.h
@@ -18,4 +18,12 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-int	 cert_new(const char *, const char *, const char *, int);
+struct iri;
+
+extern char	**identities;
+
+int		 certs_init(const char *);
+const char	*ccert(const char *);
+const char	*cert_for(struct iri *);
+int		 cert_open(const char *);
+int		 cert_new(const char *, const char *, const char *, int);
blob - 7758bf12a823fbfb255a8158a88bddba18e2b018
blob + ceff70ecc8ba50abc5c4395df2443de8e6bfc829
--- fs.c
+++ fs.c
@@ -67,6 +67,8 @@ char		known_hosts_file[PATH_MAX], known_hosts_tmp[PATH
 char		crashed_file[PATH_MAX];
 char		session_file[PATH_MAX], session_file_tmp[PATH_MAX];
 char		history_file[PATH_MAX], history_file_tmp[PATH_MAX];
+char		cert_dir[PATH_MAX];
+char		certs_file[PATH_MAX], certs_file_tmp[PATH_MAX];
 
 char		cwd[PATH_MAX];
 
@@ -370,8 +372,16 @@ fs_init(void)
 	    sizeof(history_file));
 	join_path(history_file_tmp, cache_path_base, "/history.XXXXXXXXXX",
 	    sizeof(history_file_tmp));
+	join_path(cert_dir, data_path_base, "/certs/",
+	    sizeof(cert_dir));
+	join_path(certs_file, data_path_base, "/certs/certs",
+	    sizeof(certs_file));
+	join_path(certs_file_tmp, data_path_base, "/certs/certs.XXXXXXXXXX",
+	    sizeof(certs_file_tmp));
 	join_path(crashed_file, cache_path_base, "/crashed",
 	    sizeof(crashed_file));
 
+	mkdirs(cert_dir, S_IRWXU);
+
 	return 1;
 }
blob - c18c4d95757e523512b6cf129160fa9189ceb0cb
blob + 2d642755e7c9fcc3e659e4ff7f6a99dd6dbf3282
--- fs.h
+++ fs.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021, 2022 Omar Polo <op@omarpolo.com>
+ * Copyright (c) 2021, 2022, 2024 Omar Polo <op@omarpolo.com>
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -32,6 +32,8 @@ extern char	known_hosts_file[PATH_MAX], known_hosts_tm
 extern char	crashed_file[PATH_MAX];
 extern char	session_file[PATH_MAX], session_file_tmp[PATH_MAX];
 extern char	history_file[PATH_MAX], history_file_tmp[PATH_MAX];
+extern char	cert_dir[PATH_MAX];
+extern char	certs_file[PATH_MAX], paths_file_tmp[PATH_MAX];
 
 extern char	cwd[PATH_MAX];
 
blob - 48bb3e9ad2ce05481bcf18a40d6b024a8f2b7149
blob + 29378bfc1ba17d17a75d8ca398c17d206e6c407c
--- net.c
+++ net.c
@@ -16,8 +16,10 @@
 
 #include "compat.h"
 
+#include <sys/mman.h>
 #include <sys/types.h>
 #include <sys/socket.h>
+#include <sys/stat.h>
 
 #include <netinet/in.h>
 
@@ -51,6 +53,9 @@ struct req {
 	char			*port;
 	char			*req;
 	size_t			 len;
+	void			*ccert;
+	size_t			 ccert_len;
+	int			 ccert_fd;
 	int			 done_header;
 	struct bufferevent	*bev;
 
@@ -191,17 +196,28 @@ done:
 
 		tls_config_insecure_noverifycert(conf);
 		tls_config_insecure_noverifyname(conf);
+
+		if (req->ccert && tls_config_set_keypair_mem(conf,
+		    req->ccert, req->ccert_len, req->ccert, req->ccert_len)
+		    == -1) {
+			close_with_errf(req, "failed to load keypair: %s",
+			    tls_config_error(conf));
+			tls_config_free(conf);
+			return;
+		}
 
 		/* prepare tls */
 		if ((req->ctx = tls_client()) == NULL) {
 			close_with_errf(req, "tls_client: %s",
 			    strerror(errno));
+			tls_config_free(conf);
 			return;
 		}
 
 		if (tls_configure(req->ctx, conf) == -1) {
 			close_with_errf(req, "tls_configure: %s",
 			    tls_error(req->ctx));
+			tls_config_free(conf);
 			return;
 		}
 		tls_config_free(conf);
@@ -302,6 +318,11 @@ close_conn(int fd, short ev, void *d)
 
 		tls_free(req->ctx);
 		req->ctx = NULL;
+	}
+
+	if (req->ccert != NULL) {
+		munmap(req->ccert, req->ccert_len);
+		close(req->ccert_fd);
 	}
 
 	free(req->host);
@@ -632,6 +653,38 @@ net_error(struct bufferevent *bev, short error, void *
 	}
 
 	close_with_errf(req, "unknown event error %x", error);
+}
+
+static int
+load_cert(struct imsg *imsg, struct req *req)
+{
+	struct stat	 sb;
+	int		 fd;
+
+	if ((fd = imsg_get_fd(imsg)) == -1)
+		return (0);
+
+	if (fstat(fd, &sb) == -1)
+		return (-1);
+
+#if 0
+	if (sb.st_size >= (off_t)SIZE_MAX) {
+		close(fd);
+		return (-1);
+	}
+#endif
+
+	req->ccert = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+	if (req->ccert == MAP_FAILED) {
+		req->ccert = NULL;
+		close(fd);
+		return (-1);
+	}
+
+	req->ccert_len = sb.st_size;
+	req->ccert_fd = fd;
+
+	return (0);
 }
 
 static void
@@ -678,6 +731,7 @@ handle_dispatch_imsg(int fd, short event, void *d)
 			if ((req = calloc(1, sizeof(*req))) == NULL)
 				die();
 
+			req->ccert_fd = -1;
 			req->id = imsg_get_id(&imsg);
 			TAILQ_INSERT_HEAD(&reqhead, req, reqs);
 
@@ -687,6 +741,8 @@ handle_dispatch_imsg(int fd, short event, void *d)
 				die();
 			if ((req->req = strdup(r.req)) == NULL)
 				die();
+			if (load_cert(&imsg, req) == -1)
+				die();
 
 			req->len = strlen(req->req);
 
blob - 503a74eafeedeff94d7961e9fce4d014934399bc
blob + d85f1fb9da735aeff95a101a3e527eba316a0285
--- sandbox.c
+++ sandbox.c
@@ -31,7 +31,7 @@
 void
 sandbox_net_process(void)
 {
-	if (pledge("stdio inet dns", NULL) == -1)
+	if (pledge("stdio inet dns recvfd", NULL) == -1)
 		err(1, "pledge");
 }
 
@@ -57,7 +57,7 @@ sandbox_ui_process(void)
 	if (unveil(cache_path_base, "rwc") == -1)
 		err(1, "unveil(%s)", cache_path_base);
 
-	if (pledge("stdio rpath wpath cpath unix tty", NULL) == -1)
+	if (pledge("stdio rpath wpath cpath unix sendfd tty", NULL) == -1)
 		err(1, "pledge");
 }
 
blob - c17f3b88353b860490d35ebc7d8832291d82c0fb
blob + 55ec2474b63d18f22c3f76662ac98092bef60623
--- session.c
+++ session.c
@@ -166,7 +166,7 @@ free_tab(struct tab *tab)
 void
 stop_tab(struct tab *tab)
 {
-	ui_send_net(IMSG_STOP, tab->id, NULL, 0);
+	ui_send_net(IMSG_STOP, tab->id, -1, NULL, 0);
 }
 
 static inline void
blob - d90ada56333478b10894e8e96d5344f09128009a
blob + c50a70155a36fb4151dc2ecd9f8e6e49345412d1
--- telescope.c
+++ telescope.c
@@ -30,6 +30,7 @@
 #include <string.h>
 #include <unistd.h>
 
+#include "certs.h"
 #include "control.h"
 #include "defaults.h"
 #include "fs.h"
@@ -124,7 +125,7 @@ static void		 load_gopher_url(struct tab *, const char
 static void		 load_via_proxy(struct tab *, const char *,
 			     struct proxy *);
 static void		 make_request(struct tab *, struct get_req *, int,
-			     const char *);
+			     const char *, int);
 static void		 do_load_url(struct tab *, const char *, const char *, int);
 static pid_t		 start_child(enum telescope_process, const char *, int);
 static void		 send_url(const char *);
@@ -218,7 +219,7 @@ handle_imsg_check_cert(struct imsg *imsg)
 		else
 			tab->trust = TS_TRUSTED;
 
-		ui_send_net(IMSG_CERT_STATUS, imsg->hdr.peerid,
+		ui_send_net(IMSG_CERT_STATUS, imsg->hdr.peerid, -1,
 		    &tofu_res, sizeof(tofu_res));
 	} else {
 		tab->trust = TS_UNTRUSTED;
@@ -233,7 +234,7 @@ handle_imsg_check_cert(struct imsg *imsg)
 static void
 handle_check_cert_user_choice(int accept, struct tab *tab)
 {
-	ui_send_net(IMSG_CERT_STATUS, tab->id, &accept,
+	ui_send_net(IMSG_CERT_STATUS, tab->id, -1, &accept,
 	    sizeof(accept));
 
 	if (accept) {
@@ -349,7 +350,7 @@ handle_request_response(struct tab *tab)
 	} else if (tab->code == 20) {
 		history_add(hist_cur(tab->hist));
 		if (setup_parser_for(tab)) {
-			ui_send_net(IMSG_PROCEED, tab->id, NULL, 0);
+			ui_send_net(IMSG_PROCEED, tab->id, -1, NULL, 0);
 		} else if (safe_mode) {
 			load_page_from_str(tab,
 			    err_pages[UNKNOWN_TYPE_OR_CSET]);
@@ -419,7 +420,7 @@ handle_save_page_path(const char *path, struct tab *ta
 	ui_show_downloads_pane();
 	d = enqueue_download(tab->id, path);
 	d->fd = fd;
-	ui_send_net(IMSG_PROCEED, d->id, NULL, 0);
+	ui_send_net(IMSG_PROCEED, d->id, -1, NULL, 0);
 
 	/*
 	 * Change this tab id, the old one is associated with the
@@ -592,19 +593,23 @@ load_finger_url(struct tab *tab, const char *url)
 	strlcat(req.req, "\r\n", sizeof(req.req));
 
 	parser_init(tab, textplain_initparser);
-	make_request(tab, &req, PROTO_FINGER, NULL);
+	make_request(tab, &req, PROTO_FINGER, NULL, 0);
 }
 
 static void
 load_gemini_url(struct tab *tab, const char *url)
 {
 	struct get_req	 req;
+	int		 use_cert = 0;
 
+	if ((tab->client_cert = cert_for(&tab->iri)) != NULL)
+		use_cert = 1;
+
 	memset(&req, 0, sizeof(req));
 	strlcpy(req.host, tab->iri.iri_host, sizeof(req.host));
 	strlcpy(req.port, tab->iri.iri_portstr, sizeof(req.port));
 
-	make_request(tab, &req, PROTO_GEMINI, hist_cur(tab->hist));
+	make_request(tab, &req, PROTO_GEMINI, hist_cur(tab->hist), use_cert);
 }
 
 static inline const char *
@@ -675,7 +680,7 @@ load_gopher_url(struct tab *tab, const char *url)
 	}
 	strlcat(req.req, "\r\n", sizeof(req.req));
 
-	make_request(tab, &req, PROTO_GOPHER, NULL);
+	make_request(tab, &req, PROTO_GOPHER, NULL, 0);
 }
 
 static void
@@ -689,12 +694,15 @@ load_via_proxy(struct tab *tab, const char *url, struc
 
 	tab->proxy = p;
 
-	make_request(tab, &req, p->proto, hist_cur(tab->hist));
+	make_request(tab, &req, p->proto, hist_cur(tab->hist), 0);
 }
 
 static void
-make_request(struct tab *tab, struct get_req *req, int proto, const char *r)
+make_request(struct tab *tab, struct get_req *req, int proto, const char *r,
+    int use_cert)
 {
+	int	 fd = -1;
+
 	stop_tab(tab);
 	tab->id = tab_new_id();
 	req->proto = proto;
@@ -705,7 +713,15 @@ make_request(struct tab *tab, struct get_req *req, int
 	}
 
 	start_loading_anim(tab);
-	ui_send_net(IMSG_GET, tab->id, req, sizeof(*req));
+
+	if (!use_cert)
+		tab->client_cert = NULL;
+	if (use_cert && (fd = cert_open(tab->client_cert)) == -1) {
+		tab->client_cert = NULL;
+		message("failed to open certificate: %s", strerror(errno));
+	}
+
+	ui_send_net(IMSG_GET, tab->id, fd, req, sizeof(*req));
 }
 
 void
@@ -731,7 +747,7 @@ gopher_send_search_req(struct tab *tab, const char *te
 	erase_buffer(&tab->buffer);
 	parser_init(tab, gophermap_initparser);
 
-	make_request(tab, &req, PROTO_GOPHER, NULL);
+	make_request(tab, &req, PROTO_GOPHER, NULL, 0);
 }
 
 void
@@ -1003,10 +1019,10 @@ send_url(const char *url)
 }
 
 int
-ui_send_net(int type, uint32_t peerid, const void *data,
+ui_send_net(int type, uint32_t peerid, int fd, const void *data,
     uint16_t datalen)
 {
-	return imsg_compose_event(iev_net, type, peerid, 0, -1, data,
+	return imsg_compose_event(iev_net, type, peerid, 0, fd, data,
 	    datalen);
 }
 
@@ -1091,6 +1107,7 @@ main(int argc, char * const *argv)
 	TAILQ_INIT(&minibuffer_map.m);
 
 	fs_init();
+	certs_init(certs_file);
 	config_init();
 	parseconfig(config_path, fail);
 	if (configtest) {
@@ -1172,7 +1189,7 @@ main(int argc, char * const *argv)
 		ui_end();
 	}
 
-	ui_send_net(IMSG_QUIT, 0, NULL, 0);
+	ui_send_net(IMSG_QUIT, 0, -1, NULL, 0);
 	imsg_flush(&iev_net->ibuf);
 
 	/* wait for children to terminate */
blob - c077f15294e3d9cac921621ca5d65ee35d53d4e3
blob + 471779df8f10998d8404cc3f546f80621e82ddd9
--- telescope.h
+++ telescope.h
@@ -205,6 +205,7 @@ struct tab {
 
 	char			*cert;
 	enum trust_state	 trust;
+	const char		*client_cert;
 	struct proxy		*proxy;
 	struct iri		 iri;
 	struct hist		*hist;
@@ -300,7 +301,7 @@ int		 load_next_page(struct tab*);
 void		 write_buffer(const char *, struct tab *);
 void		 humanify_url(const char *, const char *, char *, size_t);
 int		 bookmark_page(const char *);
-int		 ui_send_net(int, uint32_t, const void *, uint16_t);
+int		 ui_send_net(int, uint32_t, int, const void *, uint16_t);
 
 /* tofu.c */
 void			 tofu_init(struct ohash*, unsigned int, ptrdiff_t);
blob - 2c16045881aaa24551c4373ab324470e474c29c0
blob + 55e20314797f9773fe0430a7448c59bdf1374329
--- ui.c
+++ ui.c
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ * Copyright (c) 2021, 2024 Omar Polo <op@omarpolo.com>
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -846,9 +846,10 @@ redraw_modeline(struct tab *tab)
 	wattr_on(modeline, modeline_face.background, NULL);
 	wmove(modeline, 0, 0);
 
-	wprintw(modeline, "-%c%c- %s ",
+	wprintw(modeline, "-%c%c%c %s ",
 	    spin[tab->loading_anim_step],
 	    trust_status_char(tab->trust),
+	    tab->client_cert ? 'C' : '-',
 	    mode == NULL ? "(none)" : mode);
 
 	pct = (buffer->line_off + buffer->curs_y) * 100.0