commit d35e18b31b0e05c6178a6bfa891dd2e2dadf3db1 from: Omar Polo date: Sun Feb 04 17:10:30 2024 UTC first draft of client certificate support At the moment telescope loads a mapping host:port/path -> certificate from a file and always uses it, no ways to change it, use a temporary one, generate a new one, etc are provided yet. The format of ~/.telescope/certs/certs is host port path certificate file name where the certificate file name is the name of a file inside ~/.telescope/certs. ~/.telescope/certs/ is ~/.local/share/telescope/ when using XDG. 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 + +#include +#include +#include #include #include @@ -33,12 +42,251 @@ #include #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 + * Copyright (c) 2021, 2022, 2024 Omar Polo * * 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 #include #include +#include #include @@ -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 #include +#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 + * Copyright (c) 2021, 2024 Omar Polo * * 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