mirror of
https://git.sr.ht/~sircmpwn/gmni
synced 2024-11-26 10:53:46 +01:00
all: rewrite with BearSSL rather than OpenSSL
This commit is contained in:
parent
863c41dba6
commit
57064dd01f
@ -8,7 +8,7 @@ This is a [Gemini](https://gemini.circumlunar.space/) client. Included are:
|
||||
Dependencies:
|
||||
|
||||
- A POSIX-like system and a C11 compiler
|
||||
- OpenSSL
|
||||
- [BearSSL](https://www.bearssl.org/index.html)
|
||||
- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional)
|
||||
|
||||
Features:
|
||||
|
@ -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
|
||||
|
@ -1,7 +1,7 @@
|
||||
#ifndef GEMINI_CLIENT_H
|
||||
#define GEMINI_CLIENT_H
|
||||
#include <bearssl_ssl.h>
|
||||
#include <netdb.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
@ -52,20 +52,16 @@ 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;
|
||||
|
||||
// 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;
|
||||
@ -75,6 +71,8 @@ struct gemini_options {
|
||||
struct addrinfo *hints;
|
||||
};
|
||||
|
||||
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 +82,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 +136,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);
|
||||
|
@ -1,9 +1,7 @@
|
||||
#ifndef GEMINI_TOFU_H
|
||||
#define GEMINI_TOFU_H
|
||||
#include <bearssl_x509.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
|
||||
|
@ -1,5 +1,7 @@
|
||||
#ifndef GEMINI_UTIL_H
|
||||
#define GEMINI_UTIL_H
|
||||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
struct pathspec {
|
||||
const char *var;
|
||||
|
162
src/client.c
162
src/client.c
@ -1,15 +1,15 @@
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <netdb.h>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <bearssl_ssl.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#include <gmni/gmni.h>
|
||||
#include <gmni/tofu.h>
|
||||
#include <gmni/url.h>
|
||||
|
||||
static enum gemini_result
|
||||
@ -88,9 +88,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 +160,50 @@ 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;
|
||||
}
|
||||
|
||||
BIO_set_ssl(sbio, resp->ssl, 0);
|
||||
|
||||
resp->bio = BIO_new(BIO_f_buffer());
|
||||
BIO_push(resp->bio, sbio);
|
||||
// TODO: session reuse
|
||||
resp->sc = &tofu->sc;
|
||||
br_ssl_client_reset(resp->sc, host, 0);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (r < 3 || strcmp(&buf[r - 2], "\r\n") != 0) {
|
||||
fprintf(stderr, "invalid line %d '%s'\n", r, buf);
|
||||
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 (l < 3 || strcmp(&buf[l-2], "\r\n") != 0) {
|
||||
fprintf(stderr, "invalid line '%s'\n", buf);
|
||||
res = GEMINI_ERR_PROTOCOL;
|
||||
goto cleanup;
|
||||
}
|
||||
@ -217,9 +215,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 +235,18 @@ gemini_response_finish(struct gemini_response *resp)
|
||||
return;
|
||||
}
|
||||
|
||||
if (resp->bio) {
|
||||
BIO_free_all(resp->bio);
|
||||
resp->bio = NULL;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
resp->ssl = NULL;
|
||||
resp->ssl_ctx = NULL;
|
||||
free(resp->meta);
|
||||
|
||||
if (resp->sc) {
|
||||
br_sslio_close(&resp->body);
|
||||
}
|
||||
|
||||
resp->sc = NULL;
|
||||
resp->meta = NULL;
|
||||
}
|
||||
|
||||
@ -277,11 +267,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:
|
||||
|
19
src/gmni.c
19
src/gmni.c
@ -1,9 +1,8 @@
|
||||
#include <assert.h>
|
||||
#include <bearssl_ssl.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>
|
||||
@ -222,10 +221,7 @@ 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();
|
||||
@ -242,7 +238,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 +337,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 +364,6 @@ next:
|
||||
gemini_response_finish(&resp);
|
||||
}
|
||||
|
||||
SSL_CTX_free(opts.ssl_ctx);
|
||||
curl_url_cleanup(url);
|
||||
gemini_tofu_finish(&cfg.tofu);
|
||||
return ret;
|
||||
|
95
src/gmnlm.c
95
src/gmnlm.c
@ -1,22 +1,25 @@
|
||||
#include <assert.h>
|
||||
#include <bearssl_ssl.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <getopt.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 <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
#include <gmni/gmni.h>
|
||||
#include <gmni/tofu.h>
|
||||
#include <gmni/url.h>
|
||||
#include <libgen.h>
|
||||
#include <limits.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 "util.h"
|
||||
|
||||
struct link {
|
||||
@ -346,10 +349,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) {
|
||||
@ -393,23 +399,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")) {
|
||||
@ -419,14 +421,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;
|
||||
resp->fd = fd;
|
||||
resp->sc = NULL;
|
||||
return GEMINI_OK;
|
||||
}
|
||||
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;
|
||||
@ -752,12 +754,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;
|
||||
|
||||
@ -945,10 +958,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) {
|
||||
@ -1123,11 +1139,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;
|
||||
@ -1189,7 +1201,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);
|
||||
|
13
src/parser.c
13
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) {
|
||||
|
246
src/tofu.c
246
src/tofu.c
@ -1,95 +1,97 @@
|
||||
#include <assert.h>
|
||||
#include <bearssl_hash.h>
|
||||
#include <bearssl_x509.h>
|
||||
#include <errno.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 <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <gmni/gmni.h>
|
||||
#include <gmni/tofu.h>
|
||||
#include <libgen.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.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;
|
||||
struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx;
|
||||
cc->server_name = server_name;
|
||||
cc->err = 0;
|
||||
cc->pkey = 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;
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
if (length == 0) {
|
||||
cc->err = BR_ERR_X509_TRUNCATED;
|
||||
return;
|
||||
}
|
||||
br_x509_decoder_init(&cc->decoder, NULL, NULL);
|
||||
br_sha512_init(&cc->sha512);
|
||||
}
|
||||
|
||||
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);
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (br_x509_decoder_isCA(&cc->decoder)) {
|
||||
return;
|
||||
}
|
||||
cc->pkey = br_x509_decoder_get_pkey(&cc->decoder);
|
||||
br_sha512_out(&cc->sha512, &cc->hash);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
char fingerprint[512 / 8 * 3];
|
||||
for (size_t i = 0; i < sizeof(md); ++i) {
|
||||
for (size_t i = 0; i < sizeof(cc->hash); ++i) {
|
||||
snprintf(&fingerprint[i * 3], 4, "%02X%s",
|
||||
md[i], i + 1 == sizeof(md) ? "" : ":");
|
||||
cc->hash[i], i + 1 == sizeof(cc->hash) ? "" : ":");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
rc = X509_check_host(cert, servername, strlen(servername), 0, NULL);
|
||||
if (rc != 1) {
|
||||
rc = X509_V_ERR_HOSTNAME_MISMATCH;
|
||||
goto invalid_cert;
|
||||
}
|
||||
|
||||
time_t now;
|
||||
time(&now);
|
||||
|
||||
enum tofu_error error = TOFU_UNTRUSTED_CERT;
|
||||
host = tofu->known_hosts;
|
||||
(void)error;
|
||||
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 +104,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;
|
||||
|
||||
invalid_cert:
|
||||
error = TOFU_INVALID_CERT;
|
||||
goto callback;
|
||||
assert(0); // Unreachable
|
||||
}
|
||||
|
||||
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 +199,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 +218,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 +240,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;
|
||||
|
@ -1,5 +1,7 @@
|
||||
#include <assert.h>
|
||||
#include <bearssl_ssl.h>
|
||||
#include <errno.h>
|
||||
#include <gmni/gmni.h>
|
||||
#include <libgen.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
@ -7,7 +9,6 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <gmni/gmni.h>
|
||||
#include "util.h"
|
||||
|
||||
static void
|
||||
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user