diff --git a/README.md b/README.md

index aa9f811a9bfd9cfb7b6654954e33fc62e868f8c4..fa803f99158582d982ef3a01d6b87471fb27f0cb 100644

--- a/README.md

+++ b/README.md

@@ -10,6 +10,7 @@

- Page history

- Regex searches

- Bookmarks

+- basic Client Certificate support (no autocreation of client certs currently)

## Non-Features:

@@ -61,6 +62,5 @@

### 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

index 424cbda346869c7476859d55a600c414c86bbb46..87888929b98a46887dbcb25378385183e4ef2566 100644

--- a/config.sh

+++ b/config.sh

@@ -117,8 +117,8 @@ echo no

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

index 70a19b193d6e33b41b5cc7ef13511bb0f5b4e076..6abccdd18d0b85e4b2d4b527d49727d360dec5c3 100755

--- 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 @@ }

cgmnlm() {

genrules cgmnlm \

+ src/certs.c \

src/client.c \

src/escape.c \

src/gmnlm.c \

@@ -25,6 +27,7 @@ }

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

index 1f20672f1fb0c8ff6a4f3a97a3324a25d2a0a01d..866dd418a510ccd141d9c23afb4b321b84a5f9ec 100644

--- a/doc/gmni.scd

+++ b/doc/gmni.scd

@@ -38,11 +38,9 @@ If the server requests user input, _path_ is opened and read, and a

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

new file mode 100644

index 0000000000000000000000000000000000000000..53b8aad27fa50649ddfdacbd08724d5d0b033120

--- /dev/null

+++ 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

index 7e27b489d71fd3a43ca60292b17d56cab3caa5f8..22295e20fb36d492a979c060de8dcdca14df5a34 100644

--- 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,7 +71,13 @@

// 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 @@ // Caller must call gemini_response_finish afterwards to clean up resources

// 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_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

index a88167ba0fb6606b2b170e5005c55131f1861972..51d1d60a3719469a1f8cd295b9d85e21d7affb43 100644

--- 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 @@ };

struct known_host {

char *host, *fingerprint;

- time_t expires;

int lineno;

struct known_host *next;

};

@@ -34,7 +31,23 @@ // certificate. Return true to trust this certificate.

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 @@ tofu_callback_t *callback;

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

index 6193fdf48c0ac6d313de7f35a1d09ecba62e3283..6c241f41a175c3d27390ecb6ca5b041bd637cb2b 100644

--- 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 @@ const char *path;

};

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

new file mode 100644

index 0000000000000000000000000000000000000000..f40bfa7d27925baaa2fe6dbdf0d6c18ce4e10014

--- /dev/null

+++ 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

index a8a12f3cfbe1e98fd2045eec257cfaf95c319910..127a56ca59859e645fea6f1e4ac62431d734e4fd 100644

--- 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 @@

#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 @@ free(host);

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;

+ }

}

- 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 +235,9 @@ fprintf(stderr, "invalid status\n");

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 @@ if (!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 @@ return gai_strerror(resp->status);

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

index 49abb8524a012c27249006dc45f4688a846e63c3..f3015ac679eba77397fb4aac5068f3a63e1cdbab 100644

--- 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/socket.h>

#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 @@

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 @@ }

}

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 @@ usage(argv[0]);

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 @@ char *buf;

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 @@

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

index e28bf70afc5d6fd4c5c6eff85d85198d62a127e8..ef4c97950495e369e4fd3b1256c185fdb01f8cf1 100644

--- 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 @@ close(pfd[0]);

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 @@ {

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 @@ resp->status = GEMINI_STATUS_BAD_REQUEST;

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 @@ resp->meta = strdup("application/x-octet-stream");

}

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 @@ }

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 @@ "TEMPORARY FAILURE" : "PERMANENT FAILURE",

resp->status, resp->meta);

break;

case GEMINI_STATUS_CLASS_SUCCESS:

- return res;

+ goto out;

}

if (requesting) {

@@ -537,6 +596,11 @@ gemini_response_finish(resp);

}

}

+out:

+ if (client_cert.key) {

+ free(client_cert.key);

+ }

+ free(scheme);

return res;

}

@@ -842,12 +906,23 @@ }

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 @@ ioctl(fileno(browser->tty), TIOCGWINSZ, &ws);

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 @@ } else {

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 @@ while (hist && hist->prev) {

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

index ad2c0e6306872f8dde450063ba8815e2ee7e314a..6f12456ddbe5248b59ea146464a1d76365505c56 100644

--- 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 @@ {

if (!p) {

return;

}

- BIO_free(p->f);

free(p->buf);

}

@@ -42,7 +43,7 @@ p->buf = realloc(p->buf, p->bufsz);

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

index 48395c08cdbf68b31c5defd15b360f1eb2897a3f..54183a79278de45bfbe5a1f8ddbcbf10be1c01c4 100644

--- 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;

+ 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 || cc->pkey) {

+ return;

}

- if (day > 0 || sec > 0) {

- rc = X509_V_ERR_CERT_NOT_YET_VALID;

- goto invalid_cert;

+ if (length == 0) {

+ cc->err = BR_ERR_X509_TRUNCATED;

+ return;

}

- if (!ASN1_TIME_diff(&day, &sec, NULL, notAfter)) {

- rc = X509_V_ERR_UNSPECIFIED;

- goto invalid_cert;

+ 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;

}

- if (day < 0 || sec < 0) {

- rc = X509_V_ERR_CERT_HAS_EXPIRED;

- goto invalid_cert;

+ 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);

+}

- 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);

-

- 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_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);

+}

- 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 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;

}

- rc = X509_check_host(cert, servername, strlen(servername), 0, NULL);

- if (rc != 1) {

- rc = X509_V_ERR_HOSTNAME_MISMATCH;

- goto invalid_cert;

+ 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) ? "" : ":");

}

- time_t now;

- time(&now);

-

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 @@ free(path_fmt);

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 @@ n = 0;

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);

@@ -207,10 +234,6 @@

tok = strtok(NULL, " ");

assert(tok);

host->fingerprint = strdup(tok);

-

- tok = strtok(NULL, " ");

- assert(tok);

- host->expires = strtoul(tok, NULL, 10);

host->lineno = lineno++;

diff --git a/src/util.c b/src/util.c

index 2f62c29ce40ac012993e1d208d65491b56d204aa..8441b584ff199ffd81472539a59acce5d0a18f42 100644

--- 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 <stdio.h>

#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 @@ }

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;