gmni

a gemini line mode client
git clone https://git.clttr.info/gmni.git
Log (Feed) | Files | Refs (Tags) | README | LICENSE

tofu.c (5892B)


      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 		snprintf(&fingerprint[i * 3], 4, "%02X%s",
     83 			cc->hash[i], i + 1 == sizeof(cc->hash) ? "" : ":");
     84 	}
     85 
     86 	enum tofu_error error = TOFU_UNTRUSTED_CERT;
     87 	struct known_host *host = cc->store->known_hosts;
     88 	while (host) {
     89 		if (strcmp(host->host, cc->server_name) != 0) {
     90 			goto next;
     91 		}
     92 		if (strcmp(host->fingerprint, fingerprint) == 0) {
     93 			// Valid match in known hosts
     94 			return 0;
     95 		}
     96 		error = TOFU_FINGERPRINT_MISMATCH;
     97 		break;
     98 next:
     99 		host = host->next;
    100 	}
    101 
    102 	switch (cc->store->callback(error, fingerprint,
    103 				host, cc->store->cb_data)) {
    104 	case TOFU_ASK:
    105 		assert(0); // Invariant
    106 	case TOFU_FAIL:
    107 		return BR_ERR_X509_NOT_TRUSTED;
    108 	case TOFU_TRUST_ONCE:
    109 		// No further action necessary
    110 		return 0;
    111 	case TOFU_TRUST_ALWAYS:;
    112 		FILE *f = fopen(cc->store->known_hosts_path, "a");
    113 		if (!f) {
    114 			fprintf(stderr, "Error opening %s for writing: %s\n",
    115 				cc->store->known_hosts_path, strerror(errno));
    116 			break;
    117 		};
    118 		fprintf(f, "%s %s %s\n", cc->server_name,
    119 				"SHA-512", fingerprint);
    120 		fclose(f);
    121 
    122 		host = calloc(1, sizeof(struct known_host));
    123 		host->host = strdup(cc->server_name);
    124 		host->fingerprint = strdup(fingerprint);
    125 		host->lineno = ++cc->store->lineno;
    126 		host->next = cc->store->known_hosts;
    127 		cc->store->known_hosts = host;
    128 		return 0;
    129 	}
    130 
    131 	assert(0); // Unreachable
    132 }
    133 
    134 static const br_x509_pkey *
    135 xt_get_pkey(const br_x509_class *const *ctx, unsigned *usages)
    136 {
    137 	struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx;
    138 	if (cc->err != 0) {
    139 		return NULL;
    140 	}
    141 	if (usages) {
    142 		// XXX: BearSSL doesn't pull the usages out of the X.509 for us
    143 		*usages = BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN;
    144 	}
    145 	return cc->pkey;
    146 }
    147 
    148 const br_x509_class xt_vtable = {
    149 	sizeof(struct x509_tofu_context),
    150 	xt_start_chain,
    151 	xt_start_cert,
    152 	xt_append,
    153 	xt_end_cert,
    154 	xt_end_chain,
    155 	xt_get_pkey,
    156 };
    157 
    158 static void
    159 x509_init_tofu(struct x509_tofu_context *ctx, struct gemini_tofu *store)
    160 {
    161 	ctx->vtable = &xt_vtable;
    162 	ctx->store = store;
    163 }
    164 
    165 void
    166 gemini_tofu_init(struct gemini_tofu *tofu, tofu_callback_t *cb, void *cb_data)
    167 {
    168 	const struct pathspec paths[] = {
    169 		{.var = "GMNIDATA", .path = "/%s"},
    170 		{.var = "XDG_DATA_HOME", .path = "/gmni/%s"},
    171 		{.var = "HOME", .path = "/.local/share/gmni/%s"}
    172 	};
    173 	char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0]));
    174 	char dname[PATH_MAX+1];
    175 	size_t n = 0;
    176 
    177 	n = snprintf(tofu->known_hosts_path,
    178 		sizeof(tofu->known_hosts_path),
    179 		path_fmt, "known_hosts");
    180 	free(path_fmt);
    181 	assert(n < sizeof(tofu->known_hosts_path));
    182 
    183 	posix_dirname(tofu->known_hosts_path, dname);
    184 	if (mkdirs(dname, 0755) != 0) {
    185 		fprintf(stderr, "Error creating directory %s: %s\n", dname,
    186 			strerror(errno));
    187 		return;
    188 	}
    189 
    190 	tofu->callback = cb;
    191 	tofu->cb_data = cb_data;
    192 
    193 	tofu->known_hosts = NULL;
    194 
    195 	x509_init_tofu(&tofu->x509_ctx, tofu);
    196 
    197 	br_x509_minimal_context _; // Discarded
    198 	br_ssl_client_init_full(&tofu->sc, &_, NULL, 0);
    199 	br_ssl_engine_set_x509(&tofu->sc.eng, &tofu->x509_ctx.vtable);
    200 	br_ssl_engine_set_buffer(&tofu->sc.eng,
    201 			&tofu->iobuf, sizeof(tofu->iobuf), 1);
    202 
    203 	FILE *f = fopen(tofu->known_hosts_path, "r");
    204 	if (!f) {
    205 		return;
    206 	}
    207 	n = 0;
    208 	int lineno = 1;
    209 	char *line = NULL;
    210 	while (getline(&line, &n, f) != -1) {
    211 		int ln = strlen(line);
    212 		if (line[ln-1] == '\n') {
    213 			line[ln-1] = 0;
    214 		}
    215 
    216 		struct known_host *host = calloc(1, sizeof(struct known_host));
    217 		char *tok = strtok(line, " ");
    218 		assert(tok);
    219 		host->host = strdup(tok);
    220 
    221 		tok = strtok(NULL, " ");
    222 		assert(tok);
    223 		if (strcmp(tok, "SHA-512") != 0) {
    224 			free(host->host);
    225 			free(host);
    226 			continue;
    227 		}
    228 
    229 		tok = strtok(NULL, " ");
    230 		assert(tok);
    231 		host->fingerprint = strdup(tok);
    232 
    233 		host->lineno = lineno++;
    234 
    235 		host->next = tofu->known_hosts;
    236 		tofu->known_hosts = host;
    237 	}
    238 	free(line);
    239 	fclose(f);
    240 }
    241 
    242 void
    243 gemini_tofu_finish(struct gemini_tofu *tofu)
    244 {
    245 	struct known_host *host = tofu->known_hosts;
    246 	while (host) {
    247 		struct known_host *tmp = host;
    248 		host = host->next;
    249 		free(tmp->host);
    250 		free(tmp->fingerprint);
    251 		free(tmp);
    252 	}
    253 }