cgmnlm

colorful gemini line mode browser
git clone https://git.clttr.info/cgmnlm.git
Log (Feed) | Files | Refs (Tags) | README | LICENSE

commit dbc726616e6675ae82d9bb55be5693371255ed2f
parent 479ea9e74f4b66645c0d7b51d99adf420d831f23
Author: René Wagner <rwagner@rw-net.de>
Date:   Mon, 15 Mar 2021 19:50:52 +0100

Merge branch 'bearssl'

Diffstat:
MREADME.md | 4++--
Mconfig.sh | 4++--
Mconfigure | 3+++
Mdoc/gmni.scd | 8+++-----
Ainclude/gmni/certs.h | 27+++++++++++++++++++++++++++
Minclude/gmni/gmni.h | 32+++++++++++++++++++++-----------
Minclude/gmni/tofu.h | 24++++++++++++++++++------
Minclude/util.h | 3+++
Asrc/certs.c | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/client.c | 176++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/gmni.c | 63++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/gmnlm.c | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/parser.c | 13+++++++------
Msrc/tofu.c | 237+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/util.c | 7++++---
15 files changed, 633 insertions(+), 283 deletions(-)

diff --git a/README.md b/README.md @@ -10,6 +10,7 @@ This is a [Gemini](https://gemini.circumlunar.space/) client. Included are: - Page history - Regex searches - Bookmarks +- basic Client Certificate support (no autocreation of client certs currently) ## Non-Features: @@ -61,6 +62,5 @@ $ make ### Dependencies: - A POSIX-like system and a C11 compiler -- OpenSSL +- [BearSSL](https://www.bearssl.org/index.html) - [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) - diff --git a/config.sh b/config.sh @@ -117,8 +117,8 @@ run_configure() { fi done - find_library OpenSSL libssl - find_library OpenSSL libcrypto + # XXX: Asked the maintainer to provide a .pc file + LIBS="$LIBS -lbearssl" printf "Checking for scdoc... " if scdoc -v >/dev/null 2>&1 diff --git a/configure b/configure @@ -4,6 +4,7 @@ eval ". $srcdir/config.sh" gmni() { genrules gmni \ + src/certs.c \ src/client.c \ src/escape.c \ src/gmni.c \ @@ -14,6 +15,7 @@ gmni() { cgmnlm() { genrules cgmnlm \ + src/certs.c \ src/client.c \ src/escape.c \ src/gmnlm.c \ @@ -25,6 +27,7 @@ cgmnlm() { libgmni_a() { genrules libgmni.a \ + src/certs.c \ src/client.c \ src/escape.c \ src/tofu.c \ diff --git a/doc/gmni.scd b/doc/gmni.scd @@ -38,11 +38,9 @@ performed with the user's input supplied to the server. second request is performed with the contents of _path_ as the user input. -*-E* _path_[:_password_] - Sets the path to the client certificate to use (and optionally a - password). If the filename contains ":" but the certificate does not - accept a password, append ":" to the path and it will be intepreted as - an empty password. +*-E* _path_:_key_ + Sets the path to the client certificate and private key file to use, + both PEM encoded. *-l* For *text/\** responses, *gmni* normally adds a line feed if stdout is a diff --git a/include/gmni/certs.h b/include/gmni/certs.h @@ -0,0 +1,27 @@ +#ifndef GEMINI_CERTS_H +#define GEMINI_CERTS_H +#include <bearssl.h> +#include <stdio.h> + +struct gmni_options; + +struct gmni_client_certificate { + br_x509_certificate *chain; + size_t nchain; + struct gmni_private_key *key; +}; + +struct gmni_private_key { + int type; + union { + br_rsa_private_key rsa; + br_ec_private_key ec; + }; + unsigned char data[]; +}; + +// Returns nonzero on failure and sets errno. Closes both files. +int gmni_ccert_load(struct gmni_client_certificate *cert, + FILE *certin, FILE *skin); + +#endif diff --git a/include/gmni/gmni.h b/include/gmni/gmni.h @@ -1,7 +1,7 @@ #ifndef GEMINI_CLIENT_H #define GEMINI_CLIENT_H +#include <bearssl.h> #include <netdb.h> -#include <openssl/ssl.h> #include <stdbool.h> #include <sys/socket.h> @@ -52,20 +52,18 @@ struct gemini_response { enum gemini_status status; char *meta; + // TODO: Make these private // Response body may be read from here if appropriate: - BIO *bio; + br_sslio_context body; // Connection state - SSL_CTX *ssl_ctx; - SSL *ssl; + br_ssl_client_context *sc; int fd; }; -struct gemini_options { - // If NULL, an SSL context will be created. If unset, the ssl field - // must also be NULL. - SSL_CTX *ssl_ctx; +struct gmni_client_certificate; +struct gemini_options { // If ai_family != AF_UNSPEC (the default value on most systems), the // client will connect to this address and skip name resolution. struct addrinfo *addr; @@ -73,8 +71,14 @@ struct gemini_options { // If non-NULL, these hints are provided to getaddrinfo. Useful, for // example, to force IPv4/IPv6. struct addrinfo *hints; + + // If non-NULL, this will be used as the client certificate for the + // request. The other fields must be set as well. + struct gmni_client_certificate *client_cert; }; +struct gemini_tofu; + // Requests the specified URL via the gemini protocol. If options is non-NULL, // it may specify some additional configuration to adjust client behavior. // @@ -84,6 +88,7 @@ struct gemini_options { // before exiting or re-using it for another request. enum gemini_result gemini_request(const char *url, struct gemini_options *options, + struct gemini_tofu *tofu, struct gemini_response *resp); // Must be called after gemini_request in order to free up the resources @@ -137,15 +142,20 @@ struct gemini_token { }; struct gemini_parser { - BIO *f; + int (*read)(void *state, void *buf, size_t nbyte); + void *state; char *buf; size_t bufsz; size_t bufln; bool preformatted; }; -// Initializes a text/gemini parser which reads from the specified BIO. -void gemini_parser_init(struct gemini_parser *p, BIO *f); +// Initializes a text/gemini parser. The provided "read" function will be called +// with the provided "state" value in order to obtain more gemtext data. The +// read function should behave like read(3). +void gemini_parser_init(struct gemini_parser *p, + int (*read)(void *state, void *buf, size_t nbyte), + void *state); // Finishes this text/gemini parser and frees up its resources. void gemini_parser_finish(struct gemini_parser *p); diff --git a/include/gmni/tofu.h b/include/gmni/tofu.h @@ -1,9 +1,7 @@ #ifndef GEMINI_TOFU_H #define GEMINI_TOFU_H +#include <bearssl.h> #include <limits.h> -#include <openssl/ssl.h> -#include <openssl/x509.h> -#include <time.h> enum tofu_error { TOFU_VALID, @@ -24,7 +22,6 @@ enum tofu_action { struct known_host { char *host, *fingerprint; - time_t expires; int lineno; struct known_host *next; }; @@ -34,7 +31,23 @@ struct known_host { typedef enum tofu_action (tofu_callback_t)(enum tofu_error error, const char *fingerprint, struct known_host *host, void *data); +struct gemini_tofu; + +struct x509_tofu_context { + const br_x509_class *vtable; + br_x509_decoder_context decoder; + br_x509_pkey *pkey; + br_sha512_context sha512; + unsigned char hash[64]; + struct gemini_tofu *store; + const char *server_name; + int err; +}; + struct gemini_tofu { + struct x509_tofu_context x509_ctx; + br_ssl_client_context sc; + unsigned char iobuf[BR_SSL_BUFSIZE_BIDI]; char known_hosts_path[PATH_MAX+1]; struct known_host *known_hosts; int lineno; @@ -42,8 +55,7 @@ struct gemini_tofu { void *cb_data; }; -void gemini_tofu_init(struct gemini_tofu *tofu, - SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); +void gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *data); void gemini_tofu_finish(struct gemini_tofu *tofu); #endif diff --git a/include/util.h b/include/util.h @@ -1,5 +1,7 @@ #ifndef GEMINI_UTIL_H #define GEMINI_UTIL_H +#include <stdio.h> +#include <sys/types.h> struct pathspec { const char *var; @@ -7,6 +9,7 @@ struct pathspec { }; char *getpath(const struct pathspec *paths, size_t npaths); +void posix_dirname(char *path, char *dname); int mkdirs(char *path, mode_t mode); int download_resp(FILE *out, struct gemini_response resp, const char *path, char *url); diff --git a/src/certs.c b/src/certs.c @@ -0,0 +1,156 @@ +#include <assert.h> +#include <bearssl.h> +#include <errno.h> +#include <gmni/certs.h> +#include <gmni/gmni.h> +#include <stdio.h> +#include <stdlib.h> + +static void +crt_append(void *ctx, const void *src, size_t len) +{ + br_x509_certificate *crt = (br_x509_certificate *)ctx; + crt->data = realloc(crt->data, crt->data_len + len); + assert(crt->data); + memcpy(&crt->data[crt->data_len], src, len); + crt->data_len += len; +} + +static void +key_append(void *ctx, const void *src, size_t len) +{ + br_skey_decoder_context *skctx = (br_skey_decoder_context *)ctx; + br_skey_decoder_push(skctx, src, len); +} + +int +gmni_ccert_load(struct gmni_client_certificate *cert, FILE *certin, FILE *skin) +{ + // TODO: Better error propagation to caller + static unsigned char buf[BUFSIZ]; + + br_pem_decoder_context pemdec; + br_pem_decoder_init(&pemdec); + + cert->chain = NULL; + cert->nchain = 0; + + static const char *certname = "CERTIFICATE"; + while (!feof(certin)) { + size_t n = fread(&buf, 1, sizeof(buf), certin); + if (ferror(certin)) { + goto error; + } + size_t q = 0; + while (q < n) { + q += br_pem_decoder_push(&pemdec, &buf[q], n - q); + switch (br_pem_decoder_event(&pemdec)) { + case BR_PEM_BEGIN_OBJ: + if (strcmp(br_pem_decoder_name(&pemdec), certname) != 0) { + break; + } + cert->chain = realloc(cert->chain, + sizeof(br_x509_certificate) * (cert->nchain + 1)); + memset(&cert->chain[cert->nchain], 0, sizeof(*cert->chain)); + br_pem_decoder_setdest(&pemdec, &crt_append, + &cert->chain[cert->nchain]); + ++cert->nchain; + break; + case BR_PEM_END_OBJ: + break; + case BR_PEM_ERROR: + fprintf(stderr, "Error decoding PEM certificate\n"); + errno = EINVAL; + goto error; + } + } + } + + if (cert->nchain == 0) { + fprintf(stderr, "No certificates found in provided client certificate file\n"); + errno = EINVAL; + goto error; + } + + br_skey_decoder_context skdec = {0}; + br_skey_decoder_init(&skdec); + br_pem_decoder_init(&pemdec); + + // TODO: Better validation of PEM file + while (!feof(skin)) { + size_t n = fread(&buf, 1, sizeof(buf), skin); + if (ferror(skin)) { + goto error; + } + size_t q = 0; + while (q < n) { + q += br_pem_decoder_push(&pemdec, &buf[q], n - q); + switch (br_pem_decoder_event(&pemdec)) { + case BR_PEM_BEGIN_OBJ: + br_pem_decoder_setdest(&pemdec, &key_append, &skdec); + break; + case BR_PEM_END_OBJ: + // no-op + break; + case BR_PEM_ERROR: + fprintf(stderr, "Error decoding PEM private key\n"); + errno = EINVAL; + goto error; + } + } + } + + int err = br_skey_decoder_last_error(&skdec); + if (err != 0) { + fprintf(stderr, "Error loading private key: %d\n", err); + errno = EINVAL; + goto error; + } + switch (br_skey_decoder_key_type(&skdec)) { + struct gmni_private_key *k; + const br_ec_private_key *ec; + const br_rsa_private_key *rsa; + case BR_KEYTYPE_RSA: + rsa = br_skey_decoder_get_rsa(&skdec); + cert->key = k = malloc(sizeof(*k) + + rsa->plen + rsa->qlen + + rsa->dplen + rsa->dqlen + + rsa->iqlen); + assert(k); + k->type = BR_KEYTYPE_RSA; + k->rsa = *rsa; + k->rsa.p = k->data; + k->rsa.q = k->rsa.p + k->rsa.plen; + k->rsa.dp = k->rsa.q + k->rsa.qlen; + k->rsa.dq = k->rsa.dp + k->rsa.dplen; + k->rsa.iq = k->rsa.dq + k->rsa.dqlen; + memcpy(k->rsa.p, rsa->p, rsa->plen); + memcpy(k->rsa.q, rsa->q, rsa->qlen); + memcpy(k->rsa.dp, rsa->dp, rsa->dplen); + memcpy(k->rsa.dq, rsa->dq, rsa->dqlen); + memcpy(k->rsa.iq, rsa->iq, rsa->iqlen); + break; + case BR_KEYTYPE_EC: + ec = br_skey_decoder_get_ec(&skdec); + cert->key = k = malloc(sizeof(*k) + ec->xlen); + assert(k); + k->type = BR_KEYTYPE_EC; + k->ec.curve = ec->curve; + k->ec.x = k->data; + k->ec.xlen = ec->xlen; + memcpy(k->ec.x, ec->x, ec->xlen); + break; + default: + assert(0); + } + + fclose(certin); + fclose(skin); + return 0; + +error: + fclose(certin); + fclose(skin); + free(cert->chain); + return 1; +} diff --git a/src/client.c b/src/client.c @@ -1,15 +1,16 @@ #include <assert.h> #include <errno.h> #include <netdb.h> -#include <openssl/bio.h> -#include <openssl/err.h> -#include <openssl/ssl.h> +#include <bearssl.h> #include <stdlib.h> +#include <stdio.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> +#include <gmni/certs.h> #include <gmni/gmni.h> +#include <gmni/tofu.h> #include <gmni/url.h> static enum gemini_result @@ -88,9 +89,41 @@ gemini_connect(struct Curl_URL *uri, struct gemini_options *options, #define GEMINI_META_MAXLEN 1024 #define GEMINI_STATUS_MAXLEN 2 +static int +sock_read(void *ctx, unsigned char *buf, size_t len) +{ + for (;;) { + ssize_t rlen; + rlen = read(*(int *)ctx, buf, len); + if (rlen <= 0) { + if (rlen < 0 && errno == EINTR) { + continue; + } + return -1; + } + return (int)rlen; + } +} + +static int +sock_write(void *ctx, const unsigned char *buf, size_t len) +{ + for (;;) { + ssize_t wlen; + wlen = write(*(int *)ctx, buf, len); + if (wlen <= 0) { + if (wlen < 0 && errno == EINTR) { + continue; + } + return -1; + } + return (int)wlen; + } +} + enum gemini_result gemini_request(const char *url, struct gemini_options *options, - struct gemini_response *resp) + struct gemini_tofu *tofu, struct gemini_response *resp) { assert(url); assert(resp); @@ -128,84 +161,69 @@ gemini_request(const char *url, struct gemini_options *options, goto cleanup; } - if (options && options->ssl_ctx) { - resp->ssl_ctx = options->ssl_ctx; - SSL_CTX_up_ref(options->ssl_ctx); - } else { - resp->ssl_ctx = SSL_CTX_new(TLS_method()); - assert(resp->ssl_ctx); - SSL_CTX_set_verify(resp->ssl_ctx, SSL_VERIFY_PEER, NULL); - } - int r; - BIO *sbio = BIO_new(BIO_f_ssl()); res = gemini_connect(uri, options, resp, &resp->fd); if (res != GEMINI_OK) { free(host); goto cleanup; } - resp->ssl = SSL_new(resp->ssl_ctx); - assert(resp->ssl); - SSL_set_connect_state(resp->ssl); - if ((r = SSL_set1_host(resp->ssl, host)) != 1) { - free(host); - goto ssl_error; - } - if ((r = SSL_set_tlsext_host_name(resp->ssl, host)) != 1) { - free(host); - goto ssl_error; - } - free(host); - if ((r = SSL_set_fd(resp->ssl, resp->fd)) != 1) { - goto ssl_error; - } - if ((r = SSL_connect(resp->ssl)) != 1) { - goto ssl_error; - } - - X509 *cert = SSL_get_peer_certificate(resp->ssl); - if (!cert) { - resp->status = X509_V_ERR_UNSPECIFIED; - res = GEMINI_ERR_SSL_VERIFY; - goto cleanup; - } - X509_free(cert); - - long vr = SSL_get_verify_result(resp->ssl); - if (vr != X509_V_OK) { - resp->status = vr; - res = GEMINI_ERR_SSL_VERIFY; - goto cleanup; + // TODO: session reuse + resp->sc = &tofu->sc; + if (options->client_cert) { + struct gmni_client_certificate *cert = options->client_cert; + struct gmni_private_key *key = cert->key; + switch (key->type) { + case BR_KEYTYPE_RSA: + br_ssl_client_set_single_rsa(resp->sc, + cert->chain, cert->nchain, &key->rsa, + br_rsa_pkcs1_sign_get_default()); + break; + case BR_KEYTYPE_EC: + br_ssl_client_set_single_ec(resp->sc, + cert->chain, cert->nchain, &key->ec, + BR_KEYTYPE_SIGN, 0, + br_ec_get_default(), + br_ecdsa_sign_asn1_get_default()); + break; + } } + br_ssl_client_reset(resp->sc, host, 0); - BIO_set_ssl(sbio, resp->ssl, 0); - - resp->bio = BIO_new(BIO_f_buffer()); - BIO_push(resp->bio, sbio); + br_sslio_init(&resp->body, &resp->sc->eng, + sock_read, &resp->fd, sock_write, &resp->fd); char req[1024 + 3]; r = snprintf(req, sizeof(req), "%s\r\n", url); assert(r > 0); - r = BIO_puts(sbio, req); - if (r == -1) { - res = GEMINI_ERR_IO; - goto cleanup; - } - assert(r == (int)strlen(req)); + br_sslio_write_all(&resp->body, req, r); + br_sslio_flush(&resp->body); + // The SSL engine maintains an internal buffer, so this shouldn't be as + // inefficient as it looks. It's necessary to do this one byte at a time + // to avoid consuming any of the response body buffer. char buf[GEMINI_META_MAXLEN + GEMINI_STATUS_MAXLEN + 2 /* CRLF */ + 1 /* NUL */]; - r = BIO_gets(resp->bio, buf, sizeof(buf)); - if (r == -1) { - res = GEMINI_ERR_IO; - goto cleanup; + memset(buf, 0, sizeof(buf)); + size_t l; + for (l = 0; l < 2 || memcmp(&buf[l-2], "\r\n", 2) != 0; ++l) { + r = br_sslio_read(&resp->body, &buf[l], 1); + if (r < 0) { + break; + } + } + + int err = br_ssl_engine_last_error(&resp->sc->eng); + if (err != 0) { + // TODO: Bubble this up properly + fprintf(stderr, "SSL error %d\n", err); + goto ssl_error; } - if (r < 3 || strcmp(&buf[r - 2], "\r\n") != 0) { - fprintf(stderr, "invalid line %d '%s'\n", r, buf); + if (l < 3 || strcmp(&buf[l-2], "\r\n") != 0) { + fprintf(stderr, "invalid line '%s'\n", buf); res = GEMINI_ERR_PROTOCOL; goto cleanup; } @@ -217,9 +235,9 @@ gemini_request(const char *url, struct gemini_options *options, res = GEMINI_ERR_PROTOCOL; goto cleanup; } - resp->meta = calloc(r - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1); - strncpy(resp->meta, &endptr[1], r - 5); - resp->meta[r - 5] = '\0'; + resp->meta = calloc(l - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1); + strncpy(resp->meta, &endptr[1], l - 5); + resp->meta[l - 5] = '\0'; cleanup: curl_url_cleanup(uri); @@ -237,26 +255,18 @@ gemini_response_finish(struct gemini_response *resp) return; } - if (resp->bio) { - BIO_free_all(resp->bio); - resp->bio = NULL; + if (resp->fd != -1) { + close(resp->fd); + resp->fd = -1; } - if (resp->ssl) { - SSL_free(resp->ssl); - } - if (resp->ssl_ctx) { - SSL_CTX_free(resp->ssl_ctx); - } free(resp->meta); - if (resp->fd != -1) { - close(resp->fd); - resp->fd = -1; + if (resp->sc) { + br_sslio_close(&resp->body); } - resp->ssl = NULL; - resp->ssl_ctx = NULL; + resp->sc = NULL; resp->meta = NULL; } @@ -277,11 +287,11 @@ gemini_strerr(enum gemini_result r, struct gemini_response *resp) case GEMINI_ERR_CONNECT: return strerror(errno); case GEMINI_ERR_SSL: - return ERR_error_string( - SSL_get_error(resp->ssl, resp->status), - NULL); + // TODO: more specific + return "SSL error"; case GEMINI_ERR_SSL_VERIFY: - return X509_verify_cert_error_string(resp->status); + // TODO: more specific + return "X.509 certificate not trusted"; case GEMINI_ERR_IO: return "I/O error"; case GEMINI_ERR_PROTOCOL: diff --git a/src/gmni.c b/src/gmni.c @@ -1,9 +1,8 @@ #include <assert.h> +#include <bearssl.h> #include <errno.h> #include <getopt.h> #include <netdb.h> -#include <openssl/bio.h> -#include <openssl/err.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> @@ -12,6 +11,7 @@ #include <sys/types.h> #include <termios.h> #include <unistd.h> +#include <gmni/certs.h> #include <gmni/gmni.h> #include <gmni/tofu.h> #include <gmni/url.h> @@ -110,6 +110,45 @@ tofu_callback(enum tofu_error error, const char *fingerprint, return action; } +static struct gmni_client_certificate * +load_client_cert(char *argv_0, char *path) +{ + char *certpath = strtok(path, ":"); + if (!certpath) { + usage(argv_0); + exit(1); + } + + FILE *certf = fopen(certpath, "r"); + if (!certf) { + fprintf(stderr, "Failed to open certificate: %s\n", + strerror(errno)); + exit(1); + } + + char *keypath = strtok(NULL, ":"); + if (!keypath) { + usage(argv_0); + exit(1); + } + + FILE *keyf = fopen(keypath, "r"); + if (!keyf) { + fprintf(stderr, "Failed to open certificate: %s\n", + strerror(errno)); + exit(1); + } + + struct gmni_client_certificate *cert = + calloc(1, sizeof(struct gmni_client_certificate)); + if (gmni_ccert_load(cert, certf, keyf) != 0) { + fprintf(stderr, "Failed to load client certificate: %s\n", + strerror(errno)); + exit(1); + } + return cert; +} + int main(int argc, char *argv[]) { @@ -166,7 +205,7 @@ main(int argc, char *argv[]) } break; case 'E': - assert(0); // TODO: Client certificates + opts.client_cert = load_client_cert(argv[0], optarg); break; case 'h': usage(argv[0]); @@ -222,15 +261,12 @@ main(int argc, char *argv[]) return 1; } - SSL_load_error_strings(); - ERR_load_crypto_strings(); - opts.ssl_ctx = SSL_CTX_new(TLS_method()); - gemini_tofu_init(&cfg.tofu, opts.ssl_ctx, &tofu_callback, &cfg); + gemini_tofu_init(&cfg.tofu, &tofu_callback, &cfg); bool exit = false; struct Curl_URL *url = curl_url(); - if(curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { + if (curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { // TODO: Better error fprintf(stderr, "Error: invalid URL\n"); return 1; @@ -242,7 +278,8 @@ main(int argc, char *argv[]) curl_url_get(url, CURLUPART_URL, &buf, 0); struct gemini_response resp; - enum gemini_result r = gemini_request(buf, &opts, &resp); + enum gemini_result r = gemini_request(buf, + &opts, &cfg.tofu, &resp); free(buf); @@ -340,11 +377,8 @@ main(int argc, char *argv[]) char last = 0; char buf[BUFSIZ]; for (int n = 1; n > 0;) { - n = BIO_read(resp.bio, buf, BUFSIZ); - if (n == -1) { - fprintf(stderr, "Error: read\n"); - return 1; - } else if (n != 0) { + n = br_sslio_read(&resp.body, buf, BUFSIZ); + if (n > 0) { last = buf[n - 1]; } ssize_t w = 0; @@ -370,7 +404,6 @@ next: gemini_response_finish(&resp); } - SSL_CTX_free(opts.ssl_ctx); curl_url_cleanup(url); gemini_tofu_finish(&cfg.tofu); return ret; diff --git a/src/gmnlm.c b/src/gmnlm.c @@ -1,22 +1,26 @@ #include <assert.h> +#include <bearssl.h> #include <ctype.h> +#include <errno.h> +#include <fcntl.h> #include <getopt.h> +#include <gmni/certs.h> +#include <gmni/gmni.h> +#include <gmni/tofu.h> +#include <gmni/url.h> #include <libgen.h> #include <limits.h> -#include <openssl/bio.h> -#include <openssl/err.h> #include <regex.h> #include <stdbool.h> #include <stdio.h> +#include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/stat.h> +#include <sys/types.h> #include <sys/wait.h> #include <termios.h> #include <unistd.h> -#include <gmni/gmni.h> -#include <gmni/tofu.h> -#include <gmni/url.h> #include "util.h" #define ANSI_COLOR_RED "\x1b[91m" @@ -400,10 +404,13 @@ pipe_resp(FILE *out, struct gemini_response resp, char *cmd) { FILE *f = fdopen(pfd[1], "w"); // XXX: may affect history, do we care? for (int n = 1; n > 0;) { - n = BIO_read(resp.bio, buf, BUFSIZ); - if (n == -1) { - fprintf(stderr, "Error: read\n"); - return; + if (resp.sc) { + n = br_sslio_read(&resp.body, buf, BUFSIZ); + } else { + n = read(resp.fd, buf, BUFSIZ); + } + if (n < 0) { + n = 0; } ssize_t w = 0; while (w < (ssize_t)n) { @@ -430,13 +437,50 @@ do_requests(struct browser *browser, struct gemini_response *resp) int nredir = 0; bool requesting = true; enum gemini_result res; - while (requesting) { - char *scheme; + + char *scheme; + CURLUcode uc = curl_url_get(browser->url, + CURLUPART_SCHEME, &scheme, 0); + assert(uc == CURLUE_OK); // Invariant + + char *host = NULL; + struct gmni_client_certificate client_cert = {0}; + const struct pathspec paths[] = { + {.var = "GMNIDATA", .path = "/certs/%s.%s"}, + {.var = "XDG_DATA_HOME", .path = "/gmni/certs/%s.%s"}, + {.var = "HOME", .path = "/.local/share/gmni/certs/%s.%s"} + }; + char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); + char certpath[PATH_MAX+1], keypath[PATH_MAX+1]; + size_t n = 0; + + if (strcmp(scheme, "gemini") == 0) { CURLUcode uc = curl_url_get(browser->url, - CURLUPART_SCHEME, &scheme, 0); - assert(uc == CURLUE_OK); // Invariant + CURLUPART_HOST, &host, 0); + assert(uc == CURLUE_OK); + + n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); + assert(n < sizeof(certpath)); + FILE *certin = fopen(certpath, "r"); + if (certin) { + n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); + assert(n < sizeof(keypath)); + + FILE *skin = fopen(keypath, "r"); + if (gmni_ccert_load(&client_cert, certin, skin)) { + browser->opts.client_cert = NULL; + fprintf(stderr, "Unable to load client certificate for host %s", host); + } else { + browser->opts.client_cert = &client_cert; + } + } else { + browser->opts.client_cert = NULL; + } + free(host); + } + + while (requesting) { if (strcmp(scheme, "file") == 0) { - free(scheme); requesting = false; char *path; @@ -447,23 +491,19 @@ do_requests(struct browser *browser, struct gemini_response *resp) break; } - FILE *fp = fopen(path, "r"); - if (!fp) { + int fd = open(path, O_RDONLY); + if (fd < 0) { resp->status = GEMINI_STATUS_NOT_FOUND; - /* Make sure members of resp evaluate to false, so that - gemini_response_finish does not try to free them. */ - resp->bio = NULL; - resp->ssl = NULL; - resp->ssl_ctx = NULL; + // Make sure members of resp evaluate to false, + // so that gemini_response_finish does not try + // to free them. + resp->sc = NULL; resp->meta = NULL; resp->fd = -1; free(path); break; } - BIO *file = BIO_new_fp(fp, BIO_CLOSE); - resp->bio = BIO_new(BIO_f_buffer()); - BIO_push(resp->bio, file); if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) { resp->meta = strdup("text/gemini"); } else if (has_suffix(path, ".txt")) { @@ -473,14 +513,14 @@ do_requests(struct browser *browser, struct gemini_response *resp) } free(path); resp->status = GEMINI_STATUS_SUCCESS; - resp->fd = -1; - resp->ssl = NULL; - resp->ssl_ctx = NULL; - return GEMINI_OK; + resp->fd = fd; + resp->sc = NULL; + res = GEMINI_OK; + goto out; } - free(scheme); - res = gemini_request(browser->plain_url, &browser->opts, resp); + res = gemini_request(browser->plain_url, &browser->opts, + &browser->tofu, resp); if (res != GEMINI_OK) { fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); requesting = false; @@ -519,7 +559,26 @@ do_requests(struct browser *browser, struct gemini_response *resp) set_url(browser, resp->meta, NULL); break; case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: - assert(0); // TODO + requesting = false; + assert(host); + n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); + assert(n < sizeof(certpath)); + n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); + char dname[PATH_MAX + 1]; + posix_dirname(certpath, dname); + if (mkdirs(dname, 0755) != 0) { + fprintf(stderr, "Error creating directory %s: %s\n", + dname, strerror(errno)); + break; + } + assert(n < sizeof(keypath)); + fprintf(stderr, "The server requested a client certificate.\n" + "Presently, this process is not automated.\n" + "The following OpenSSL command will generate a certificate for this host:\n\n" + "openssl req -x509 -newkey rsa:4096 \\\n\t-keyout %s \\\n\t-out %s \\\n\t-days 36500 -nodes\n\n" + "Use the 'r' command to reload the page after creating this certificate.\n", + keypath, certpath); + break; case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: requesting = false; @@ -529,7 +588,7 @@ do_requests(struct browser *browser, struct gemini_response *resp) resp->status, resp->meta); break; case GEMINI_STATUS_CLASS_SUCCESS: - return res; + goto out; } if (requesting) { @@ -537,6 +596,11 @@ do_requests(struct browser *browser, struct gemini_response *resp) } } +out: + if (client_cert.key) { + free(client_cert.key); + } + free(scheme); return res; } @@ -842,12 +906,23 @@ wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col) return fprintf(f, "%s\n", s) - 1; } +static int +resp_read(void *state, void *buf, size_t nbyte) +{ + struct gemini_response *resp = state; + if (resp->sc) { + return br_sslio_read(&resp->body, buf, nbyte); + } else { + return read(resp->fd, buf, nbyte); + } +} + static bool display_gemini(struct browser *browser, struct gemini_response *resp) { int nlinks = 0; struct gemini_parser p; - gemini_parser_init(&p, resp->bio); + gemini_parser_init(&p, &resp_read, resp); free(browser->page_title); browser->page_title = NULL; @@ -1036,10 +1111,13 @@ display_plaintext(struct browser *browser, struct gemini_response *resp) char buf[BUFSIZ]; for (int n = 1; n > 0;) { - n = BIO_read(resp->bio, buf, BUFSIZ); - if (n == -1) { - fprintf(stderr, "Error: read\n"); - return 1; + if (resp->sc) { + n = br_sslio_read(&resp->body, buf, BUFSIZ); + } else { + n = read(resp->fd, buf, BUFSIZ); + } + if (n < 0) { + n = 0; } ssize_t w = 0; while (w < (ssize_t)n) { @@ -1214,11 +1292,7 @@ main(int argc, char *argv[]) open_bookmarks(&browser); } - SSL_load_error_strings(); - ERR_load_crypto_strings(); - browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); - gemini_tofu_init(&browser.tofu, browser.opts.ssl_ctx, - &tofu_callback, &browser); + gemini_tofu_init(&browser.tofu, &tofu_callback, &browser); struct gemini_response resp; browser.running = true; @@ -1280,7 +1354,6 @@ main(int argc, char *argv[]) hist = hist->prev; } history_free(hist); - SSL_CTX_free(browser.opts.ssl_ctx); curl_url_cleanup(browser.url); free(browser.page_title); free(browser.plain_url); diff --git a/src/parser.c b/src/parser.c @@ -1,21 +1,23 @@ #include <assert.h> #include <ctype.h> -#include <openssl/bio.h> #include <stdbool.h> #include <stddef.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> #include <gmni/gmni.h> void -gemini_parser_init(struct gemini_parser *p, BIO *f) +gemini_parser_init(struct gemini_parser *p, + int (*read)(void *state, void *buf, size_t nbyte), + void *state) { - p->f = f; + p->read = read; + p->state = state; p->bufln = 0; p->bufsz = BUFSIZ; p->buf = malloc(p->bufsz + 1); p->buf[0] = 0; - BIO_up_ref(p->f); p->preformatted = false; } @@ -25,7 +27,6 @@ gemini_parser_finish(struct gemini_parser *p) if (!p) { return; } - BIO_free(p->f); free(p->buf); } @@ -42,7 +43,7 @@ gemini_parser_next(struct gemini_parser *p, struct gemini_token *tok) assert(p->buf); } - ssize_t n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln - 1); + int n = p->read(p->state, &p->buf[p->bufln], p->bufsz - p->bufln - 1); if (n == -1) { return -1; } else if (n == 0) { diff --git a/src/tofu.c b/src/tofu.c @@ -1,95 +1,92 @@ #include <assert.h> +#include <bearssl.h> #include <errno.h> +#include <gmni/gmni.h> +#include <gmni/tofu.h> #include <libgen.h> #include <limits.h> -#include <openssl/asn1.h> -#include <openssl/evp.h> -#include <openssl/ssl.h> -#include <openssl/x509.h> -#include <openssl/x509v3.h> +#include <stdint.h> #include <stdio.h> +#include <stdlib.h> #include <string.h> -#include <time.h> -#include <gmni/gmni.h> -#include <gmni/tofu.h> #include "util.h" -static int -verify_callback(X509_STORE_CTX *ctx, void *data) +static void +xt_start_chain(const br_x509_class **ctx, const char *server_name) { - // Gemini clients handle TLS verification differently from the rest of - // the internet. We use a TOFU system, so trust is based on two factors: - // - // - Is the certificate valid at the time of the request? - // - Has the user trusted this certificate yet? - // - // If the answer to the latter is "no", then we give the user an - // opportunity to explicitly agree to trust the certificate before - // rejecting it. - // - // If you're reading this code with the intent to re-use it for - // something unrelated to Gemini, think twice. - struct gemini_tofu *tofu = (struct gemini_tofu *)data; - X509 *cert = X509_STORE_CTX_get0_cert(ctx); - struct known_host *host = NULL; - - int rc; - int day, sec; - const ASN1_TIME *notBefore = X509_get0_notBefore(cert); - const ASN1_TIME *notAfter = X509_get0_notAfter(cert); - if (!ASN1_TIME_diff(&day, &sec, NULL, notBefore)) { - rc = X509_V_ERR_UNSPECIFIED; - goto invalid_cert; - } - if (day > 0 || sec > 0) { - rc = X509_V_ERR_CERT_NOT_YET_VALID; - goto invalid_cert; - } - if (!ASN1_TIME_diff(&day, &sec, NULL, notAfter)) { - rc = X509_V_ERR_UNSPECIFIED; - goto invalid_cert; - } - if (day < 0 || sec < 0) { - rc = X509_V_ERR_CERT_HAS_EXPIRED; - goto invalid_cert; - } - - unsigned char md[512 / 8]; - const EVP_MD *sha512 = EVP_sha512(); - unsigned int len = sizeof(md); - rc = X509_digest(cert, sha512, md, &len); - assert(rc == 1); + struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; + cc->server_name = server_name; + cc->err = 0; + cc->pkey = NULL; +} - char fingerprint[512 / 8 * 3]; - for (size_t i = 0; i < sizeof(md); ++i) { - snprintf(&fingerprint[i * 3], 4, "%02X%s", - md[i], i + 1 == sizeof(md) ? "" : ":"); +static void +xt_start_cert(const br_x509_class **ctx, uint32_t length) +{ + struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; + if (cc->err != 0 || cc->pkey) { + return; + } + if (length == 0) { + cc->err = BR_ERR_X509_TRUNCATED; + return; + } + br_x509_decoder_init(&cc->decoder, NULL, NULL); + br_sha512_init(&cc->sha512); +} + +static void +xt_append(const br_x509_class **ctx, const unsigned char *buf, size_t len) +{ + struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; + if (cc->err != 0 || cc->pkey) { + return; + } + br_x509_decoder_push(&cc->decoder, buf, len); + int err = br_x509_decoder_last_error(&cc->decoder); + if (err != 0 && err != BR_ERR_X509_TRUNCATED) { + cc->err = err; } + br_sha512_update(&cc->sha512, buf, len); +} - SSL *ssl = X509_STORE_CTX_get_ex_data(ctx, - SSL_get_ex_data_X509_STORE_CTX_idx()); - const char *servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); - if (!servername) { - rc = X509_V_ERR_HOSTNAME_MISMATCH; - goto invalid_cert; +static void +xt_end_cert(const br_x509_class **ctx) +{ + struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; + if (cc->err != 0) { + return; + } + int err = br_x509_decoder_last_error(&cc->decoder); + if (err != 0 && err != BR_ERR_X509_TRUNCATED) { + cc->err = err; + return; } + cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); + br_sha512_out(&cc->sha512, &cc->hash); +} - rc = X509_check_host(cert, servername, strlen(servername), 0, NULL); - if (rc != 1) { - rc = X509_V_ERR_HOSTNAME_MISMATCH; - goto invalid_cert; +static unsigned +xt_end_chain(const br_x509_class **ctx) +{ + struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; + if (cc->err != 0) { + return (unsigned)cc->err; + } + if (!cc->pkey) { + return BR_ERR_X509_EMPTY_CHAIN; } - time_t now; - time(&now); + char fingerprint[512 / 8 * 3]; + for (size_t i = 0; i < sizeof(cc->hash); ++i) { + snprintf(&fingerprint[i * 3], 4, "%02X%s", + cc->hash[i], i + 1 == sizeof(cc->hash) ? "" : ":"); + } enum tofu_error error = TOFU_UNTRUSTED_CERT; - host = tofu->known_hosts; + struct known_host *host = cc->store->known_hosts; while (host) { - if (host->expires < now) { - goto next; - } - if (strcmp(host->host, servername) != 0) { + if (strcmp(host->host, cc->server_name) != 0) { goto next; } if (strcmp(host->fingerprint, fingerprint) == 0) { @@ -102,66 +99,84 @@ next: host = host->next; } - rc = X509_V_ERR_CERT_UNTRUSTED; - -callback: - switch (tofu->callback(error, fingerprint, host, tofu->cb_data)) { + switch (cc->store->callback(error, fingerprint, + host, cc->store->cb_data)) { case TOFU_ASK: assert(0); // Invariant case TOFU_FAIL: - X509_STORE_CTX_set_error(ctx, rc); - break; + return BR_ERR_X509_NOT_TRUSTED; case TOFU_TRUST_ONCE: // No further action necessary return 0; case TOFU_TRUST_ALWAYS:; - FILE *f = fopen(tofu->known_hosts_path, "a"); + FILE *f = fopen(cc->store->known_hosts_path, "a"); if (!f) { fprintf(stderr, "Error opening %s for writing: %s\n", - tofu->known_hosts_path, strerror(errno)); + cc->store->known_hosts_path, strerror(errno)); break; }; - struct tm expires_tm; - ASN1_TIME_to_tm(notAfter, &expires_tm); - time_t expires = mktime(&expires_tm); - fprintf(f, "%s %s %s %jd\n", servername, - "SHA-512", fingerprint, (intmax_t)expires); + fprintf(f, "%s %s %s\n", cc->server_name, + "SHA-512", fingerprint); fclose(f); host = calloc(1, sizeof(struct known_host)); - host->host = strdup(servername); + host->host = strdup(cc->server_name); host->fingerprint = strdup(fingerprint); - host->expires = expires; - host->lineno = ++tofu->lineno; - host->next = tofu->known_hosts; - tofu->known_hosts = host; + host->lineno = ++cc->store->lineno; + host->next = cc->store->known_hosts; + cc->store->known_hosts = host; return 0; } - X509_STORE_CTX_set_error(ctx, rc); - return 0; + assert(0); // Unreachable +} -invalid_cert: - error = TOFU_INVALID_CERT; - goto callback; +static const br_x509_pkey * +xt_get_pkey(const br_x509_class *const *ctx, unsigned *usages) +{ + struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; + if (cc->err != 0) { + return NULL; + } + if (usages) { + // XXX: BearSSL doesn't pull the usages out of the X.509 for us + *usages = BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN; + } + return cc->pkey; } +const br_x509_class xt_vtable = { + sizeof(struct x509_tofu_context), + xt_start_chain, + xt_start_cert, + xt_append, + xt_end_cert, + xt_end_chain, + xt_get_pkey, +}; + +static void +x509_init_tofu(struct x509_tofu_context *ctx, struct gemini_tofu *store) +{ + ctx->vtable = &xt_vtable; + ctx->store = store; +} void -gemini_tofu_init(struct gemini_tofu *tofu, - SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *cb_data) +gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *cb_data) { const struct pathspec paths[] = { {.var = "GMNIDATA", .path = "/%s"}, - {.var = "XDG_DATA_HOME", .path = "/gemini/%s"}, - {.var = "HOME", .path = "/.local/share/gemini/%s"} + {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, + {.var = "HOME", .path = "/.local/share/gmni/%s"} }; char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); char dname[PATH_MAX+1]; size_t n = 0; - n = snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); + n = snprintf(tofu->known_hosts_path, + sizeof(tofu->known_hosts_path), + path_fmt, "known_hosts"); assert(n < sizeof(tofu->known_hosts_path)); strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1); @@ -179,10 +194,17 @@ gemini_tofu_init(struct gemini_tofu *tofu, tofu->callback = cb; tofu->cb_data = cb_data; - SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu); tofu->known_hosts = NULL; + x509_init_tofu(&tofu->x509_ctx, tofu); + + br_x509_minimal_context _; // Discarded + br_ssl_client_init_full(&tofu->sc, &_, NULL, 0); + br_ssl_engine_set_x509(&tofu->sc.eng, &tofu->x509_ctx.vtable); + br_ssl_engine_set_buffer(&tofu->sc.eng, + &tofu->iobuf, sizeof(tofu->iobuf), 1); + FILE *f = fopen(tofu->known_hosts_path, "r"); if (!f) { return; @@ -191,6 +213,11 @@ gemini_tofu_init(struct gemini_tofu *tofu, int lineno = 1; char *line = NULL; while (getline(&line, &n, f) != -1) { + int ln = strlen(line); + if (line[ln-1] == '\n') { + line[ln-1] = 0; + } + struct known_host *host = calloc(1, sizeof(struct known_host)); char *tok = strtok(line, " "); assert(tok); @@ -208,10 +235,6 @@ gemini_tofu_init(struct gemini_tofu *tofu, assert(tok); host->fingerprint = strdup(tok); - tok = strtok(NULL, " "); - assert(tok); - host->expires = strtoul(tok, NULL, 10); - host->lineno = lineno++; host->next = tofu->known_hosts; diff --git a/src/util.c b/src/util.c @@ -1,5 +1,7 @@ #include <assert.h> +#include <bearssl.h> #include <errno.h> +#include <gmni/gmni.h> #include <libgen.h> #include <limits.h> #include <stdint.h> @@ -7,10 +9,9 @@ #include <stdlib.h> #include <string.h> #include <sys/stat.h> -#include <gmni/gmni.h> #include "util.h" -static void +void posix_dirname(char *path, char *dname) { char p[PATH_MAX+1]; @@ -82,7 +83,7 @@ download_resp(FILE *out, struct gemini_response resp, const char *path, fprintf(out, "Downloading %s to %s\n", url, path); char buf[BUFSIZ]; for (int n = 1; n > 0;) { - n = BIO_read(resp.bio, buf, sizeof(buf)); + n = br_sslio_read(&resp.body, buf, sizeof(buf)); if (n == -1) { fprintf(stderr, "Error: read\n"); return 1;