diff --git a/configure b/configure

index 7412cf545fa96e6be169c0055778b83a06b557f5..44db11c4765077677a1f506f034c846cc736ab8c 100755

--- a/configure

+++ b/configure

@@ -7,7 +7,9 @@ genrules gmni \

src/client.c \

src/escape.c \

src/gmni.c \

- src/url.c

+ src/tofu.c \

+ src/url.c \

+ src/util.c

}

gmnlm() {

@@ -16,6 +18,7 @@ src/client.c \

src/escape.c \

src/gmnlm.c \

src/parser.c \

+ src/tofu.c \

src/url.c \

src/util.c

}

diff --git a/doc/gmni.scd b/doc/gmni.scd

index 0d84d4d974f2c38b426f6ae85d228cdd4847cbda..2c1dc54272aaf3a4cbd6b1c13e29323f418f199b 100644

--- a/doc/gmni.scd

+++ b/doc/gmni.scd

@@ -6,7 +6,7 @@ gmni - Gemini client

# SYNPOSIS

-*gmni* [-46lLiIN] [-E _path_] [-d _input_] [-D _path_] _gemini://..._

+*gmni* [-46lLiIN] [-j _mode_] [-E _path_] [-d _input_] [-D _path_] _gemini://..._

# DESCRIPTION

@@ -51,6 +51,11 @@ this behavior.

*-L*

Follow redirects.

+

+*-j* _mode_

+ Sets the TOFU (trust on first use) configuration, which controls if the

+ client shall trust new certificates. _mode_ can be one of *always*,

+ *once*, or *fail*.

*-i*

Print the response status and meta text to stdout.

diff --git a/doc/gmnlm.scd b/doc/gmnlm.scd

index c5e7bf7f6b189f984e01ea2a942f47acb993f7e7..b11f3612e044ad0667d8c4d306445ae86e9f73d4 100644

--- a/doc/gmnlm.scd

+++ b/doc/gmnlm.scd

@@ -6,13 +6,18 @@ gmnlm - Gemini line-mode browser

# SYNPOSIS

-*gmnlm* [-PU] _gemini://..._

+*gmnlm* [-PU] [-j _mode_] _gemini://..._

# DESCRIPTION

*gmnlm* is an interactive line-mode Gemini browser.

# OPTIONS

+

+*-j* _mode_

+ Sets the TOFU (trust on first use) configuration, which controls if the

+ client shall trust new certificates. _mode_ can be one of *always*,

+ *once*, or *fail*.

*-P*

Disable pagination.

diff --git a/include/gmni.h b/include/gmni.h

index 4240c6231010ebb86aead2d49700fe2e3d00b65c..7e27b489d71fd3a43ca60292b17d56cab3caa5f8 100644

--- a/include/gmni.h

+++ b/include/gmni.h

@@ -13,6 +13,7 @@ GEMINI_ERR_NOT_GEMINI,

GEMINI_ERR_RESOLVE,

GEMINI_ERR_CONNECT,

GEMINI_ERR_SSL,

+ GEMINI_ERR_SSL_VERIFY,

GEMINI_ERR_IO,

GEMINI_ERR_PROTOCOL,

};

@@ -64,10 +65,6 @@ 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 NULL, an SSL connection will be established. If set, it is

- // presumed that the caller pre-established the SSL connection.

- SSL *ssl;

// If ai_family != AF_UNSPEC (the default value on most systems), the

// client will connect to this address and skip name resolution.

diff --git a/include/tofu.h b/include/tofu.h

new file mode 100644

index 0000000000000000000000000000000000000000..29aa9bc21567868cafb25a09dbc25ea0685ab01c

--- /dev/null

+++ b/include/tofu.h

@@ -0,0 +1,48 @@

+#ifndef GEMINI_TOFU_H

+#define GEMINI_TOFU_H

+#include <limits.h>

+#include <openssl/ssl.h>

+#include <openssl/x509.h>

+#include <time.h>

+

+enum tofu_error {

+ TOFU_VALID,

+ // Expired, wrong CN, etc.

+ TOFU_INVALID_CERT,

+ // Cert is valid but we haven't seen it before

+ TOFU_UNTRUSTED_CERT,

+ // Cert is valid but we already trust another cert for this host

+ TOFU_FINGERPRINT_MISMATCH,

+};

+

+enum tofu_action {

+ TOFU_ASK,

+ TOFU_FAIL,

+ TOFU_TRUST_ONCE,

+ TOFU_TRUST_ALWAYS,

+};

+

+struct known_host {

+ char *host, *fingerprint;

+ time_t expires;

+ int lineno;

+ struct known_host *next;

+};

+

+// Called when the user needs to be prompted to agree to trust an unknown

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

+ char known_hosts_path[PATH_MAX+1];

+ struct known_host *known_hosts;

+ int lineno;

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

+

+#endif

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

index d8b67d7b9f47b4473ba6eb278853ea0565739f09..07460f917b153b0d368eb2a98f368aa6a5df56a4 100644

--- a/src/client.c

+++ b/src/client.c

@@ -95,6 +95,7 @@ {

assert(url);

assert(resp);

resp->meta = NULL;

+ resp->bio = NULL;

if (strlen(url) > 1024) {

return GEMINI_ERR_INVALID_URL;

}

@@ -110,7 +111,7 @@ res = GEMINI_ERR_INVALID_URL;

goto cleanup;

}

- char *scheme;

+ char *scheme, *host;

if (curl_url_get(uri, CURLUPART_SCHEME, &scheme, 0) != CURLUE_OK) {

res = GEMINI_ERR_INVALID_URL;

goto cleanup;

@@ -120,6 +121,10 @@ res = GEMINI_ERR_NOT_GEMINI;

goto cleanup;

}

}

+ if (curl_url_get(uri, CURLUPART_HOST, &host, 0) != CURLUE_OK) {

+ res = GEMINI_ERR_INVALID_URL;

+ goto cleanup;

+ }

if (options && options->ssl_ctx) {

resp->ssl_ctx = options->ssl_ctx;

@@ -127,42 +132,54 @@ 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());

- if (options && options->ssl) {

- resp->ssl = options->ssl;

- SSL_up_ref(resp->ssl);

- BIO_set_ssl(sbio, resp->ssl, 0);

- resp->fd = -1;

- } else {

- res = gemini_connect(uri, options, resp, &resp->fd);

- if (res != GEMINI_OK) {

- goto cleanup;

- }

+ res = gemini_connect(uri, options, resp, &resp->fd);

+ if (res != GEMINI_OK) {

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

+ goto ssl_error;

+ }

+ if ((r = SSL_set_tlsext_host_name(resp->ssl, host)) != 1) {

+ goto ssl_error;

+ }

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

- resp->ssl = SSL_new(resp->ssl_ctx);

- assert(resp->ssl);

- int r = SSL_set_fd(resp->ssl, resp->fd);

- if (r != 1) {

- resp->status = r;

- res = GEMINI_ERR_SSL;

- goto cleanup;

- }

- r = SSL_connect(resp->ssl);

- if (r != 1) {

- resp->status = r;

- res = GEMINI_ERR_SSL;

- goto cleanup;

- }

- BIO_set_ssl(sbio, resp->ssl, 0);

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

char req[1024 + 3];

- int r = snprintf(req, sizeof(req), "%s\r\n", url);

+ r = snprintf(req, sizeof(req), "%s\r\n", url);

assert(r > 0);

r = BIO_puts(sbio, req);

@@ -199,6 +216,10 @@

cleanup:

curl_url_cleanup(uri);

return res;

+ssl_error:

+ res = GEMINI_ERR_SSL;

+ resp->status = r;

+ goto cleanup;

}

void

@@ -248,6 +269,8 @@ case GEMINI_ERR_SSL:

return ERR_error_string(

SSL_get_error(resp->ssl, resp->status),

NULL);

+ case GEMINI_ERR_SSL_VERIFY:

+ return X509_verify_cert_error_string(resp->status);

case GEMINI_ERR_IO:

return "I/O error";

case GEMINI_ERR_PROTOCOL:

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

index dc0c5c7f61679369fe4fadafd0508147868cc3c3..c13e0cd55623f557a8dc676d69aea54661b11faf 100644

--- a/src/gmni.c

+++ b/src/gmni.c

@@ -13,6 +13,7 @@ #include <sys/types.h>

#include <termios.h>

#include <unistd.h>

#include "gmni.h"

+#include "tofu.h"

static void

usage(const char *argv_0)

@@ -57,6 +58,55 @@ }

return input;

}

+struct tofu_config {

+ struct gemini_tofu tofu;

+ enum tofu_action action;

+};

+

+static enum tofu_action

+tofu_callback(enum tofu_error error, const char *fingerprint,

+ struct known_host *host, void *data)

+{

+ struct tofu_config *cfg = (struct tofu_config *)data;

+ enum tofu_action action = cfg->action;

+ switch (error) {

+ case TOFU_VALID:

+ assert(0); // Invariant

+ case TOFU_INVALID_CERT:

+ fprintf(stderr,

+ "The server presented an invalid certificate with fingerprint %s.\n",

+ fingerprint);

+ if (action == TOFU_TRUST_ALWAYS) {

+ action = TOFU_TRUST_ONCE;

+ }

+ break;

+ case TOFU_UNTRUSTED_CERT:

+ fprintf(stderr,

+ "The certificate offered by this server is of unknown trust. "

+ "Its fingerprint is: \n"

+ "%s\n\n", fingerprint);

+ break;

+ case TOFU_FINGERPRINT_MISMATCH:

+ fprintf(stderr,

+ "The certificate offered by this server DOES NOT MATCH the one we have on file.\n"

+ "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n"

+ "The unknown certificate's fingerprint is:\n"

+ "%s\n\n"

+ "The expected fingerprint is:\n"

+ "%s\n\n"

+ "If you're certain that this is correct, edit %s:%d\n",

+ fingerprint, host->fingerprint,

+ cfg->tofu.known_hosts_path, host->lineno);

+ return TOFU_FAIL;

+ }

+

+ if (action == TOFU_ASK) {

+ return TOFU_FAIL;

+ }

+

+ return action;

+}

+

int

main(int argc, char *argv[])

{

@@ -71,7 +121,6 @@ enum input_mode {

INPUT_READ,

INPUT_SUPPRESS,

};

-

enum input_mode input_mode = INPUT_READ;

FILE *input_source = stdin;

@@ -82,9 +131,11 @@ struct addrinfo hints = {0};

struct gemini_options opts = {

.hints = &hints,

};

+ struct tofu_config cfg;

+ cfg.action = TOFU_ASK;

int c;

- while ((c = getopt(argc, argv, "46d:D:E:hlLiINR:")) != -1) {

+ while ((c = getopt(argc, argv, "46d:D:E:hj:lLiINR:")) != -1) {

switch (c) {

case '4':

hints.ai_family = AF_INET;

@@ -115,6 +166,18 @@ break;

case 'h':

usage(argv[0]);

return 0;

+ case 'j':

+ if (strcmp(optarg, "fail") == 0) {

+ cfg.action = TOFU_FAIL;

+ } else if (strcmp(optarg, "once") == 0) {

+ cfg.action = TOFU_TRUST_ONCE;

+ } else if (strcmp(optarg, "always") == 0) {

+ cfg.action = TOFU_TRUST_ALWAYS;

+ } else {

+ usage(argv[0]);

+ return 1;

+ }

+ break;

case 'l':

linefeed = false;

break;

@@ -153,6 +216,8 @@ }

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

bool exit = false;

char *url = strdup(argv[optind]);

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

index 69b9a75ca1caab6f7e5f74685e550539be149ab6..41284df2235e869f49e542f5e1f2dcc240deb684 100644

--- a/src/gmnlm.c

+++ b/src/gmnlm.c

@@ -13,6 +13,7 @@ #include <sys/ioctl.h>

#include <termios.h>

#include <unistd.h>

#include "gmni.h"

+#include "tofu.h"

#include "url.h"

#include "util.h"

@@ -29,6 +30,8 @@

struct browser {

bool pagination, unicode;

struct gemini_options opts;

+ struct gemini_tofu tofu;

+ enum tofu_action tofu_mode;

FILE *tty;

char *plain_url;

@@ -657,22 +660,113 @@

return false;

}

+static enum tofu_action

+tofu_callback(enum tofu_error error, const char *fingerprint,

+ struct known_host *host, void *data)

+{

+ struct browser *browser = data;

+ if (browser->tofu_mode != TOFU_ASK) {

+ return browser->tofu_mode;

+ }

+

+ static char prompt[8192];

+ switch (error) {

+ case TOFU_VALID:

+ assert(0); // Invariant

+ case TOFU_INVALID_CERT:

+ snprintf(prompt, sizeof(prompt),

+ "The server presented an invalid certificate. If you choose to proceed, "

+ "you should not disclose personal information or trust the contents of the page.\n"

+ "trust [o]nce; [a]bort\n"

+ "=> ");

+ break;

+ case TOFU_UNTRUSTED_CERT:

+ snprintf(prompt, sizeof(prompt),

+ "The certificate offered by this server is of unknown trust. "

+ "Its fingerprint is: \n"

+ "%s\n\n"

+ "If you knew the fingerprint to expect in advance, verify that this matches.\n"

+ "Otherwise, it should be safe to trust this certificate.\n\n"

+ "[t]rust always; trust [o]nce; [a]bort\n"

+ "=> ", fingerprint);

+ break;

+ case TOFU_FINGERPRINT_MISMATCH:

+ snprintf(prompt, sizeof(prompt),

+ "The certificate offered by this server DOES NOT MATCH the one we have on file.\n"

+ "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n"

+ "The unknown certificate's fingerprint is:\n"

+ "%s\n\n"

+ "The expected fingerprint is:\n"

+ "%s\n\n"

+ "If you're certain that this is correct, edit %s:%d\n",

+ fingerprint, host->fingerprint,

+ browser->tofu.known_hosts_path, host->lineno);

+ return TOFU_FAIL;

+ }

+

+ bool prompting = true;

+ while (prompting) {

+ fprintf(browser->tty, "%s", prompt);

+

+ size_t sz = 0;

+ char *line = NULL;

+ if (getline(&line, &sz, browser->tty) == -1) {

+ free(line);

+ return TOFU_FAIL;

+ }

+ if (line[1] != '\n') {

+ free(line);

+ continue;

+ }

+

+ char c = line[0];

+ free(line);

+

+ switch (c) {

+ case 't':

+ if (error == TOFU_INVALID_CERT) {

+ break;

+ }

+ return TOFU_TRUST_ALWAYS;

+ case 'o':

+ return TOFU_TRUST_ONCE;

+ case 'a':

+ return TOFU_FAIL;

+ }

+ }

+

+ return TOFU_FAIL;

+}

+

int

main(int argc, char *argv[])

{

struct browser browser = {

.pagination = true,

+ .tofu_mode = TOFU_ASK,

.unicode = true,

.url = curl_url(),

.tty = fopen("/dev/tty", "w+"),

};

int c;

- while ((c = getopt(argc, argv, "hPU")) != -1) {

+ while ((c = getopt(argc, argv, "hj:PU")) != -1) {

switch (c) {

case 'h':

usage(argv[0]);

return 0;

+ case 'j':

+ if (strcmp(optarg, "fail") == 0) {

+ browser.tofu_mode = TOFU_FAIL;

+ } else if (strcmp(optarg, "once") == 0) {

+ browser.tofu_mode = TOFU_TRUST_ONCE;

+ } else if (strcmp(optarg, "always") == 0) {

+ browser.tofu_mode = TOFU_TRUST_ALWAYS;

+ } else {

+ usage(argv[0]);

+ return 1;

+ }

+ break;

case 'P':

browser.pagination = false;

break;

@@ -695,6 +789,8 @@

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

struct gemini_response resp;

browser.running = true;

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

new file mode 100644

index 0000000000000000000000000000000000000000..e8efeaf69fcb9bd890711f76959e86efa75cfec6

--- /dev/null

+++ b/src/tofu.c

@@ -0,0 +1,201 @@

+#include <assert.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 <stdio.h>

+#include <string.h>

+#include <time.h>

+#include "tofu.h"

+#include "util.h"

+

+static int

+verify_callback(X509_STORE_CTX *ctx, void *data)

+{

+ // 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, think

+ // twice.

+ //

+ // TODO: Check that the subject name is valid for the requested URL.

+ struct gemini_tofu *tofu = (struct gemini_tofu *)data;

+ X509 *cert = X509_STORE_CTX_get0_cert(ctx);

+

+ 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[256 / 8];

+ const EVP_MD *sha512 = EVP_sha512();

+ unsigned int len = sizeof(md);

+ rc = X509_digest(cert, sha512, md, &len);

+ assert(rc == 1);

+

+ char fingerprint[256 / 8 * 3];

+ for (size_t i = 0; i < sizeof(md); ++i) {

+ snprintf(&fingerprint[i * 3], 4, "%02X%s",

+ md[i], i + 1 == sizeof(md) ? "" : ":");

+ }

+

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

+ }

+

+ time_t now;

+ time(&now);

+

+ enum tofu_error error = TOFU_UNTRUSTED_CERT;

+ struct known_host *host = tofu->known_hosts;

+ while (host) {

+ if (host->expires < now) {

+ goto next;

+ }

+ if (strcmp(host->host, servername) != 0) {

+ goto next;

+ }

+ if (strcmp(host->fingerprint, fingerprint) == 0) {

+ // Valid match in known hosts

+ return 0;

+ }

+ error = TOFU_FINGERPRINT_MISMATCH;

+ break;

+next:

+ host = host->next;

+ }

+

+ rc = X509_V_ERR_CERT_UNTRUSTED;

+

+callback:

+ switch (tofu->callback(error, fingerprint, host, tofu->cb_data)) {

+ case TOFU_ASK:

+ assert(0); // Invariant

+ case TOFU_FAIL:

+ X509_STORE_CTX_set_error(ctx, rc);

+ break;

+ case TOFU_TRUST_ONCE:

+ // No further action necessary

+ return 0;

+ case TOFU_TRUST_ALWAYS:;

+ FILE *f = fopen(tofu->known_hosts_path, "a");

+ if (!f) {

+ fprintf(stderr, "Error opening %s for writing: %s\n",

+ tofu->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 %ld\n", servername,

+ "SHA-512", fingerprint, expires);

+ fclose(f);

+

+ host = calloc(1, sizeof(struct known_host));

+ host->host = strdup(servername);

+ host->fingerprint = strdup(fingerprint);

+ host->expires = expires;

+ host->lineno = ++tofu->lineno;

+ host->next = tofu->known_hosts;

+ tofu->known_hosts = host;

+ return 0;

+ }

+

+ X509_STORE_CTX_set_error(ctx, rc);

+ return 0;

+

+invalid_cert:

+ error = TOFU_INVALID_CERT;

+ goto callback;

+}

+

+

+void

+gemini_tofu_init(struct gemini_tofu *tofu,

+ SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *cb_data)

+{

+ const struct pathspec paths[] = {

+ {.var = "GMNIDATA", .path = "/%s"},

+ {.var = "XDG_DATA_HOME", .path = "/gmni/%s"},

+ {.var = "HOME", .path = "/.local/share/gmni/%s"}

+ };

+ const char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0]));

+ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path),

+ path_fmt, "known_hosts");

+

+ if (mkdirs(dirname(tofu->known_hosts_path), 0755) != 0) {

+ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path),

+ path_fmt, "known_hosts");

+ fprintf(stderr, "Error creating directory %s: %s\n",

+ dirname(tofu->known_hosts_path), strerror(errno));

+ return;

+ }

+

+ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path),

+ path_fmt, "known_hosts");

+

+ tofu->callback = cb;

+ tofu->cb_data = cb_data;

+ SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu);

+

+ FILE *f = fopen(tofu->known_hosts_path, "r");

+ if (!f) {

+ return;

+ }

+ size_t n = 0;

+ char *line = NULL;

+ while (getline(&line, &n, f) != -1) {

+ struct known_host *host = calloc(1, sizeof(struct known_host));

+ char *tok = strtok(line, " ");

+ assert(tok);

+ host->host = strdup(tok);

+

+ tok = strtok(NULL, " ");

+ assert(tok);

+ if (strcmp(tok, "SHA-512") != 0) {

+ free(host);

+ continue;

+ }

+

+ tok = strtok(NULL, " ");

+ assert(tok);

+ host->fingerprint = strdup(tok);

+

+ tok = strtok(NULL, " ");

+ assert(tok);

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

+

+ host->next = tofu->known_hosts;

+ tofu->known_hosts = host;

+ }

+}