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
-gmni [-46lLiIN] [-E path] [-d input] [-D path] gemini://...
+gmni [-46lLiIN] [-j mode] [-E path] [-d input] [-D path] gemini://...
@@ -51,6 +51,11 @@ this behavior.
-L
Follow redirects.
+-j mode
-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
-gmnlm [-PU] gemini://...
+gmnlm [-PU] [-j mode] gemini://...
gmnlm is an interactive line-mode Gemini browser.
+-j mode
-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_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 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 {
+};
+enum tofu_action {
+};
+struct known_host {
+};
+// 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,
+struct gemini_tofu {
+};
+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;
if (strlen(url) > 1024) {
return GEMINI_ERR_INVALID_URL;
}
@@ -110,7 +111,7 @@ res = GEMINI_ERR_INVALID_URL;
goto cleanup;
}
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;
}
}
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);
}
BIO *sbio = BIO_new(BIO_f_ssl());
resp->ssl = options->ssl;
SSL_up_ref(resp->ssl);
BIO_set_ssl(sbio, resp->ssl, 0);
resp->fd = -1;
res = gemini_connect(uri, options, resp, &resp->fd);
if (res != GEMINI_OK) {
goto cleanup;
}
goto cleanup;
goto ssl_error;
goto ssl_error;
goto ssl_error;
goto ssl_error;
resp->status = X509_V_ERR_UNSPECIFIED;
res = GEMINI_ERR_SSL_VERIFY;
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->status = vr;
res = GEMINI_ERR_SSL_VERIFY;
goto cleanup;
}
resp->bio = BIO_new(BIO_f_buffer());
BIO_push(resp->bio, sbio);
char req[1024 + 3];
assert(r > 0);
r = BIO_puts(sbio, req);
@@ -199,6 +216,10 @@
cleanup:
curl_url_cleanup(uri);
return res;
+ssl_error:
}
void
@@ -248,6 +269,8 @@ case GEMINI_ERR_SSL:
return ERR_error_string(
SSL_get_error(resp->ssl, resp->status),
NULL);
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 {
+};
+static enum tofu_action
+tofu_callback(enum tofu_error error, const char *fingerprint,
+{
assert(0); // Invariant
fprintf(stderr,
"The server presented an invalid certificate with fingerprint %s.\n",
fingerprint);
if (action == TOFU_TRUST_ALWAYS) {
action = TOFU_TRUST_ONCE;
}
break;
fprintf(stderr,
"The certificate offered by this server is of unknown trust. "
"Its fingerprint is: \n"
"%s\n\n", fingerprint);
break;
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;
return TOFU_FAIL;
+}
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,
};
int c;
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();
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;
FILE *tty;
char *plain_url;
@@ -657,22 +660,113 @@
return false;
}
+static enum tofu_action
+tofu_callback(enum tofu_error error, const char *fingerprint,
+{
return browser->tofu_mode;
assert(0); // Invariant
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;
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;
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;
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;
}
+}
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;
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());
&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)
+{
rc = X509_V_ERR_UNSPECIFIED;
goto invalid_cert;
rc = X509_V_ERR_CERT_NOT_YET_VALID;
goto invalid_cert;
rc = X509_V_ERR_UNSPECIFIED;
goto invalid_cert;
rc = X509_V_ERR_CERT_HAS_EXPIRED;
goto invalid_cert;
snprintf(&fingerprint[i * 3], 4, "%02X%s",
md[i], i + 1 == sizeof(md) ? "" : ":");
SSL_get_ex_data_X509_STORE_CTX_idx());
rc = X509_V_ERR_HOSTNAME_MISMATCH;
goto invalid_cert;
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;
+callback:
assert(0); // Invariant
X509_STORE_CTX_set_error(ctx, rc);
break;
// No further action necessary
return 0;
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;
+invalid_cert:
+}
+void
+gemini_tofu_init(struct gemini_tofu *tofu,
+{
{.var = "GMNIDATA", .path = "/%s"},
{.var = "XDG_DATA_HOME", .path = "/gmni/%s"},
{.var = "HOME", .path = "/.local/share/gmni/%s"}
path_fmt, "known_hosts");
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;
path_fmt, "known_hosts");
return;
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;
+}
text/gemini
This content has been proxied by September (ba2dc).