commit 02f6af661513683f0c6c1465c5ff1dd8f03a30c9
parent 30660fc160a15504274d40d4a5ec1b31539f8c2f
Author: Drew DeVault <sir@cmpwn.com>
Date: Mon, 21 Sep 2020 15:37:24 -0400
Implement TOFU
Diffstat:
M | configure | | | 5 | ++++- |
M | doc/gmni.scd | | | 7 | ++++++- |
M | doc/gmnlm.scd | | | 7 | ++++++- |
M | include/gmni.h | | | 5 | +---- |
A | include/tofu.h | | | 48 | ++++++++++++++++++++++++++++++++++++++++++++++++ |
M | src/client.c | | | 77 | ++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------- |
M | src/gmni.c | | | 69 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
M | src/gmnlm.c | | | 98 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
A | src/tofu.c | | | 201 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
9 files changed, 480 insertions(+), 37 deletions(-)
diff --git a/configure b/configure
@@ -7,7 +7,9 @@ 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 @@ gmnlm() {
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
@@ -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
@@ -52,6 +52,11 @@ performed with the user's input supplied to the server.
*-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
@@ -6,7 +6,7 @@ gmnlm - Gemini line-mode browser
# SYNPOSIS
-*gmnlm* [-PU] _gemini://..._
+*gmnlm* [-PU] [-j _mode_] _gemini://..._
# DESCRIPTION
@@ -14,6 +14,11 @@ gmnlm - Gemini line-mode 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
@@ -13,6 +13,7 @@ enum gemini_result {
GEMINI_ERR_RESOLVE,
GEMINI_ERR_CONNECT,
GEMINI_ERR_SSL,
+ GEMINI_ERR_SSL_VERIFY,
GEMINI_ERR_IO,
GEMINI_ERR_PROTOCOL,
};
@@ -65,10 +66,6 @@ struct gemini_options {
// 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.
struct addrinfo *addr;
diff --git a/include/tofu.h 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
@@ -95,6 +95,7 @@ gemini_request(const char *url, struct gemini_options *options,
assert(url);
assert(resp);
resp->meta = NULL;
+ resp->bio = NULL;
if (strlen(url) > 1024) {
return GEMINI_ERR_INVALID_URL;
}
@@ -110,7 +111,7 @@ gemini_request(const char *url, struct gemini_options *options,
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 @@ gemini_request(const char *url, struct gemini_options *options,
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 @@ gemini_request(const char *url, struct gemini_options *options,
} 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);
- 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);
+ 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);
+
+ 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 @@ gemini_request(const char *url, struct gemini_options *options,
cleanup:
curl_url_cleanup(uri);
return res;
+ssl_error:
+ res = GEMINI_ERR_SSL;
+ resp->status = r;
+ goto cleanup;
}
void
@@ -248,6 +269,8 @@ gemini_strerr(enum gemini_result r, struct gemini_response *resp)
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
@@ -13,6 +13,7 @@
#include <termios.h>
#include <unistd.h>
#include "gmni.h"
+#include "tofu.h"
static void
usage(const char *argv_0)
@@ -57,6 +58,55 @@ get_input(const struct gemini_response *resp, FILE *source)
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 @@ main(int argc, char *argv[])
INPUT_READ,
INPUT_SUPPRESS,
};
-
enum input_mode input_mode = INPUT_READ;
FILE *input_source = stdin;
@@ -82,9 +131,11 @@ main(int argc, char *argv[])
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 @@ main(int argc, char *argv[])
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 @@ main(int argc, char *argv[])
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
@@ -13,6 +13,7 @@
#include <termios.h>
#include <unistd.h>
#include "gmni.h"
+#include "tofu.h"
#include "url.h"
#include "util.h"
@@ -29,6 +30,8 @@ struct history {
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 @@ do_requests(struct browser *browser, struct gemini_response *resp)
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 @@ main(int argc, char *argv[])
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
@@ -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;
+ }
+}