1
0
Fork 0
mirror of https://git.sr.ht/~sircmpwn/gmni synced 2024-06-10 21:56:05 +02:00
gmni/src/tofu.c

236 lines
5.7 KiB
C
Raw Normal View History

2020-09-21 21:37:24 +02:00
#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>
2020-09-27 17:40:49 +02:00
#include <openssl/x509v3.h>
2020-09-21 21:37:24 +02:00
#include <stdio.h>
#include <string.h>
#include <time.h>
2020-10-24 19:40:39 +02:00
#include <gmni/gmni.h>
#include <gmni/tofu.h>
2020-09-21 21:37:24 +02:00
#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.
//
2020-09-27 17:54:32 +02:00
// If you're reading this code with the intent to re-use it for
// something unrelated to Gemini, think twice.
2020-09-21 21:37:24 +02:00
struct gemini_tofu *tofu = (struct gemini_tofu *)data;
X509 *cert = X509_STORE_CTX_get0_cert(ctx);
2020-09-22 04:22:18 +02:00
struct known_host *host = NULL;
2020-09-21 21:37:24 +02:00
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;
}
2020-09-28 00:06:51 +02:00
unsigned char md[512 / 8];
2020-09-21 21:37:24 +02:00
const EVP_MD *sha512 = EVP_sha512();
unsigned int len = sizeof(md);
rc = X509_digest(cert, sha512, md, &len);
assert(rc == 1);
2020-09-28 00:06:51 +02:00
char fingerprint[512 / 8 * 3];
2020-09-21 21:37:24 +02:00
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;
}
2020-09-27 17:40:49 +02:00
rc = X509_check_host(cert, servername, strlen(servername), 0, NULL);
if (rc != 1) {
rc = X509_V_ERR_HOSTNAME_MISMATCH;
goto invalid_cert;
}
2020-09-21 21:37:24 +02:00
time_t now;
time(&now);
enum tofu_error error = TOFU_UNTRUSTED_CERT;
2020-09-22 04:22:18 +02:00
host = tofu->known_hosts;
2020-09-21 21:37:24 +02:00
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 %jd\n", servername,
"SHA-512", fingerprint, (intmax_t)expires);
2020-09-21 21:37:24 +02:00
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 = "/gemini/%s"},
{.var = "HOME", .path = "/.local/share/gemini/%s"}
2020-09-21 21:37:24 +02:00
};
2020-09-23 18:50:25 +02:00
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),
2020-09-21 21:37:24 +02:00
path_fmt, "known_hosts");
assert(n < sizeof(tofu->known_hosts_path));
2020-09-21 21:37:24 +02:00
strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1);
if (mkdirs(dname, 0755) != 0) {
2020-09-21 21:37:24 +02:00
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");
2020-09-23 18:50:25 +02:00
free(path_fmt);
2020-09-21 21:37:24 +02:00
tofu->callback = cb;
tofu->cb_data = cb_data;
SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu);
tofu->known_hosts = NULL;
2020-09-21 21:37:24 +02:00
FILE *f = fopen(tofu->known_hosts_path, "r");
if (!f) {
return;
}
n = 0;
int lineno = 1;
2020-09-21 21:37:24 +02:00
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) {
2020-09-23 18:50:25 +02:00
free(host->host);
2020-09-21 21:37:24 +02:00
free(host);
continue;
}
tok = strtok(NULL, " ");
assert(tok);
host->fingerprint = strdup(tok);
tok = strtok(NULL, " ");
assert(tok);
host->expires = strtoul(tok, NULL, 10);
host->lineno = lineno++;
2020-09-21 21:37:24 +02:00
host->next = tofu->known_hosts;
tofu->known_hosts = host;
}
2020-09-23 18:50:25 +02:00
free(line);
fclose(f);
}
void
gemini_tofu_finish(struct gemini_tofu *tofu)
{
struct known_host *host = tofu->known_hosts;
while (host) {
struct known_host *tmp = host;
host = host->next;
free(tmp->host);
free(tmp->fingerprint);
free(tmp);
}
2020-09-21 21:37:24 +02:00
}