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