cgmnlm

colorful gemini line mode browser
git clone https://git.clttr.info/cgmnlm.git
Log (Feed) | Files | Refs (Tags) | README | LICENSE

tofu.c (5988B)


      1 #include <assert.h>
      2 #include <bearssl.h>
      3 #include <errno.h>
      4 #include <gmni/gmni.h>
      5 #include <gmni/tofu.h>
      6 #include <libgen.h>
      7 #include <limits.h>
      8 #include <stdint.h>
      9 #include <stdio.h>
     10 #include <stdlib.h>
     11 #include <string.h>
     12 #include "util.h"
     13 
     14 static void
     15 xt_start_chain(const br_x509_class **ctx, const char *server_name)
     16 {
     17 	struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx;
     18 	cc->server_name = server_name;
     19 	cc->err = 0;
     20 	cc->pkey = NULL;
     21 }
     22 
     23 static void
     24 xt_start_cert(const br_x509_class **ctx, uint32_t length)
     25 {
     26 	struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx;
     27 	if (cc->err != 0 || cc->pkey) {
     28 		return;
     29 	}
     30 	if (length == 0) {
     31 		cc->err = BR_ERR_X509_TRUNCATED;
     32 		return;
     33 	}
     34 	br_x509_decoder_init(&cc->decoder, NULL, NULL);
     35 	br_sha512_init(&cc->sha512);
     36 }
     37 
     38 static void
     39 xt_append(const br_x509_class **ctx, const unsigned char *buf, size_t len)
     40 {
     41 	struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx;
     42 	if (cc->err != 0 || cc->pkey) {
     43 		return;
     44 	}
     45 	br_x509_decoder_push(&cc->decoder, buf, len);
     46 	int err = br_x509_decoder_last_error(&cc->decoder);
     47 	if (err != 0 && err != BR_ERR_X509_TRUNCATED) {
     48 		cc->err = err;
     49 	}
     50 	br_sha512_update(&cc->sha512, buf, len);
     51 }
     52 
     53 static void
     54 xt_end_cert(const br_x509_class **ctx)
     55 {
     56 	struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx;
     57 	if (cc->err != 0) {
     58 		return;
     59 	}
     60 	int err = br_x509_decoder_last_error(&cc->decoder);
     61 	if (err != 0 && err != BR_ERR_X509_TRUNCATED) {
     62 		cc->err = err;
     63 		return;
     64 	}
     65 	cc->pkey = br_x509_decoder_get_pkey(&cc->decoder);
     66 	br_sha512_out(&cc->sha512, &cc->hash);
     67 }
     68 
     69 static unsigned
     70 xt_end_chain(const br_x509_class **ctx)
     71 {
     72 	struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx;
     73 	if (cc->err != 0) {
     74 		return (unsigned)cc->err;
     75 	}
     76 	if (!cc->pkey) {
     77 		return BR_ERR_X509_EMPTY_CHAIN;
     78 	}
     79 
     80 	char fingerprint[512 / 8 * 3];
     81 	for (size_t i = 0; i < sizeof(cc->hash); ++i) {
     82 		size_t l = 4;
     83 		const char *trail = ":";
     84 		/* Last entry. */
     85 		if (i + 1 == sizeof(cc->hash)) {
     86 			l = 3;
     87 			trail = "";
     88 		}
     89 		snprintf(&fingerprint[i * 3], l, "%02X%s",
     90 			cc->hash[i], trail);
     91 	}
     92 
     93 	enum tofu_error error = TOFU_UNTRUSTED_CERT;
     94 	struct known_host *host = cc->store->known_hosts;
     95 	while (host) {
     96 		if (strcmp(host->host, cc->server_name) != 0) {
     97 			goto next;
     98 		}
     99 		if (strcmp(host->fingerprint, fingerprint) == 0) {
    100 			// Valid match in known hosts
    101 			return 0;
    102 		}
    103 		error = TOFU_FINGERPRINT_MISMATCH;
    104 		break;
    105 next:
    106 		host = host->next;
    107 	}
    108 
    109 	switch (cc->store->callback(error, fingerprint,
    110 				host, cc->store->cb_data)) {
    111 	case TOFU_ASK:
    112 		assert(0); // Invariant
    113 	case TOFU_FAIL:
    114 		return BR_ERR_X509_NOT_TRUSTED;
    115 	case TOFU_TRUST_ONCE:
    116 		// No further action necessary
    117 		return 0;
    118 	case TOFU_TRUST_ALWAYS:;
    119 		FILE *f = fopen(cc->store->known_hosts_path, "a");
    120 		if (!f) {
    121 			fprintf(stderr, "Error opening %s for writing: %s\n",
    122 				cc->store->known_hosts_path, strerror(errno));
    123 			break;
    124 		};
    125 		fprintf(f, "%s %s %s\n", cc->server_name,
    126 				"SHA-512", fingerprint);
    127 		fclose(f);
    128 
    129 		host = calloc(1, sizeof(struct known_host));
    130 		host->host = strdup(cc->server_name);
    131 		host->fingerprint = strdup(fingerprint);
    132 		host->lineno = ++cc->store->lineno;
    133 		host->next = cc->store->known_hosts;
    134 		cc->store->known_hosts = host;
    135 		return 0;
    136 	}
    137 
    138 	assert(0); // Unreachable
    139 }
    140 
    141 static const br_x509_pkey *
    142 xt_get_pkey(const br_x509_class *const *ctx, unsigned *usages)
    143 {
    144 	struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx;
    145 	if (cc->err != 0) {
    146 		return NULL;
    147 	}
    148 	if (usages) {
    149 		// XXX: BearSSL doesn't pull the usages out of the X.509 for us
    150 		*usages = BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN;
    151 	}
    152 	return cc->pkey;
    153 }
    154 
    155 const br_x509_class xt_vtable = {
    156 	sizeof(struct x509_tofu_context),
    157 	xt_start_chain,
    158 	xt_start_cert,
    159 	xt_append,
    160 	xt_end_cert,
    161 	xt_end_chain,
    162 	xt_get_pkey,
    163 };
    164 
    165 static void
    166 x509_init_tofu(struct x509_tofu_context *ctx, struct gemini_tofu *store)
    167 {
    168 	ctx->vtable = &xt_vtable;
    169 	ctx->store = store;
    170 }
    171 
    172 void
    173 gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *cb_data)
    174 {
    175 	const struct pathspec paths[] = {
    176 		{.var = "GMNIDATA", .path = "/%s"},
    177 		{.var = "XDG_DATA_HOME", .path = "/gmni/%s"},
    178 		{.var = "HOME", .path = "/.local/share/gmni/%s"}
    179 	};
    180 	char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0]));
    181 	char dname[PATH_MAX+1];
    182 	size_t n = 0;
    183 
    184 	n = snprintf(tofu->known_hosts_path,
    185 		sizeof(tofu->known_hosts_path),
    186 		path_fmt, "known_hosts");
    187 	free(path_fmt);
    188 	assert(n < sizeof(tofu->known_hosts_path));
    189 
    190 	posix_dirname(tofu->known_hosts_path, dname);
    191 	if (mkdirs(dname, 0755) != 0) {
    192 		fprintf(stderr, "Error creating directory %s: %s\n", dname,
    193 			strerror(errno));
    194 		return;
    195 	}
    196 
    197 	tofu->callback = cb;
    198 	tofu->cb_data = cb_data;
    199 
    200 	tofu->known_hosts = NULL;
    201 
    202 	x509_init_tofu(&tofu->x509_ctx, tofu);
    203 
    204 	br_x509_minimal_context _; // Discarded
    205 	br_ssl_client_init_full(&tofu->sc, &_, NULL, 0);
    206 	br_ssl_engine_set_x509(&tofu->sc.eng, &tofu->x509_ctx.vtable);
    207 	br_ssl_engine_set_buffer(&tofu->sc.eng,
    208 			&tofu->iobuf, sizeof(tofu->iobuf), 1);
    209 
    210 	FILE *f = fopen(tofu->known_hosts_path, "r");
    211 	if (!f) {
    212 		return;
    213 	}
    214 	n = 0;
    215 	int lineno = 1;
    216 	char *line = NULL;
    217 	while (getline(&line, &n, f) != -1) {
    218 		int ln = strlen(line);
    219 		if (line[ln-1] == '\n') {
    220 			line[ln-1] = 0;
    221 		}
    222 
    223 		struct known_host *host = calloc(1, sizeof(struct known_host));
    224 		char *tok = strtok(line, " ");
    225 		assert(tok);
    226 		host->host = strdup(tok);
    227 
    228 		tok = strtok(NULL, " ");
    229 		assert(tok);
    230 		if (strcmp(tok, "SHA-512") != 0) {
    231 			free(host->host);
    232 			free(host);
    233 			continue;
    234 		}
    235 
    236 		tok = strtok(NULL, " ");
    237 		assert(tok);
    238 		host->fingerprint = strdup(tok);
    239 
    240 		host->lineno = lineno++;
    241 
    242 		host->next = tofu->known_hosts;
    243 		tofu->known_hosts = host;
    244 	}
    245 	free(line);
    246 	fclose(f);
    247 }
    248 
    249 void
    250 gemini_tofu_finish(struct gemini_tofu *tofu)
    251 {
    252 	struct known_host *host = tofu->known_hosts;
    253 	while (host) {
    254 		struct known_host *tmp = host;
    255 		host = host->next;
    256 		free(tmp->host);
    257 		free(tmp->fingerprint);
    258 		free(tmp);
    259 	}
    260 }