1/*	$OpenBSD: geofeed.c,v 1.16 2024/02/21 09:17:06 tb Exp $ */
2/*
3 * Copyright (c) 2022 Job Snijders <job@fastly.com>
4 * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv>
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19#include <sys/socket.h>
20
21#include <arpa/inet.h>
22
23#include <ctype.h>
24#include <err.h>
25#include <stdlib.h>
26#include <string.h>
27#include <vis.h>
28
29#include <openssl/bio.h>
30#include <openssl/x509.h>
31
32#include "extern.h"
33
34extern ASN1_OBJECT	*geofeed_oid;
35
36/*
37 * Take a CIDR prefix (in presentation format) and add it to parse results.
38 * Returns 1 on success, 0 on failure.
39 */
40static int
41geofeed_parse_geoip(struct geofeed *geofeed, char *cidr, char *loc)
42{
43	struct geoip	*geoip;
44	struct ip_addr	*ipaddr;
45	enum afi	 afi;
46	int		 plen;
47
48	if ((ipaddr = calloc(1, sizeof(struct ip_addr))) == NULL)
49		err(1, NULL);
50
51	if ((plen = inet_net_pton(AF_INET, cidr, ipaddr->addr,
52	    sizeof(ipaddr->addr))) != -1)
53		afi = AFI_IPV4;
54	else if ((plen = inet_net_pton(AF_INET6, cidr, ipaddr->addr,
55	    sizeof(ipaddr->addr))) != -1)
56		afi = AFI_IPV6;
57	else {
58		static char buf[80];
59
60		if (strnvis(buf, cidr, sizeof(buf), VIS_SAFE)
61		    >= (int)sizeof(buf)) {
62			memcpy(buf + sizeof(buf) - 4, "...", 4);
63		}
64		warnx("invalid address: %s", buf);
65		free(ipaddr);
66		return 0;
67	}
68
69	ipaddr->prefixlen = plen;
70
71	geofeed->geoips = recallocarray(geofeed->geoips, geofeed->geoipsz,
72	    geofeed->geoipsz + 1, sizeof(struct geoip));
73	if (geofeed->geoips == NULL)
74		err(1, NULL);
75	geoip = &geofeed->geoips[geofeed->geoipsz++];
76
77	if ((geoip->ip = calloc(1, sizeof(struct cert_ip))) == NULL)
78		err(1, NULL);
79
80	geoip->ip->type = CERT_IP_ADDR;
81	geoip->ip->ip = *ipaddr;
82	geoip->ip->afi = afi;
83
84	if ((geoip->loc = strdup(loc)) == NULL)
85		err(1, NULL);
86
87	if (!ip_cert_compose_ranges(geoip->ip))
88		return 0;
89
90	return 1;
91}
92
93/*
94 * Parse a full RFC 9092 file.
95 * Returns the Geofeed, or NULL if the object was malformed.
96 */
97struct geofeed *
98geofeed_parse(X509 **x509, const char *fn, int talid, char *buf, size_t len)
99{
100	struct geofeed	*geofeed;
101	char		*delim, *line, *loc, *nl;
102	ssize_t		 linelen;
103	BIO		*bio;
104	char		*b64 = NULL;
105	size_t		 b64sz = 0;
106	unsigned char	*der = NULL;
107	size_t		 dersz;
108	struct cert	*cert = NULL;
109	int		 rpki_signature_seen = 0, end_signature_seen = 0;
110	int		 rc = 0;
111
112	bio = BIO_new(BIO_s_mem());
113	if (bio == NULL)
114		errx(1, "BIO_new");
115
116	if ((geofeed = calloc(1, sizeof(*geofeed))) == NULL)
117		err(1, NULL);
118
119	while ((nl = memchr(buf, '\n', len)) != NULL) {
120		line = buf;
121
122		/* advance buffer to next line */
123		len -= nl + 1 - buf;
124		buf = nl + 1;
125
126		/* replace LF and CR with NUL, point nl at first NUL */
127		*nl = '\0';
128		if (nl > line && nl[-1] == '\r') {
129			nl[-1] = '\0';
130			nl--;
131			linelen = nl - line;
132		} else {
133			warnx("%s: malformed file, expected CRLF line"
134			    " endings", fn);
135			goto out;
136		}
137
138		if (end_signature_seen) {
139			warnx("%s: trailing data after signature section", fn);
140			goto out;
141		}
142
143		if (rpki_signature_seen) {
144			if (strncmp(line, "# End Signature:",
145			    strlen("# End Signature:")) == 0) {
146				end_signature_seen = 1;
147				continue;
148			}
149
150			if (linelen > 74) {
151				warnx("%s: line in signature section too long",
152				    fn);
153				goto out;
154			}
155			if (strncmp(line, "# ", strlen("# ")) != 0) {
156				warnx("%s: line in signature section too "
157				    "short", fn);
158				goto out;
159			}
160
161			/* skip over "# " */
162			line += 2;
163			strlcat(b64, line, b64sz);
164			continue;
165		}
166
167		if (strncmp(line, "# RPKI Signature:",
168		    strlen("# RPKI Signature:")) == 0) {
169			rpki_signature_seen = 1;
170
171			if ((b64 = calloc(1, len)) == NULL)
172				err(1, NULL);
173			b64sz = len;
174
175			continue;
176		}
177
178		/*
179		 * Read the Geofeed CSV records into a BIO to later on
180		 * calculate the message digest and compare with the one
181		 * in the detached CMS signature.
182		 */
183		if (BIO_puts(bio, line) != linelen ||
184		    BIO_puts(bio, "\r\n") != 2) {
185			warnx("%s: BIO_puts failed", fn);
186			goto out;
187		}
188
189		/* Zap comments and whitespace before them. */
190		delim = memchr(line, '#', linelen);
191		if (delim != NULL) {
192			while (delim > line &&
193			    isspace((unsigned char)delim[-1]))
194				delim--;
195			*delim = '\0';
196			linelen = delim - line;
197		}
198
199		/* Skip empty lines. */
200		if (linelen == 0)
201			continue;
202
203		/* Split prefix and location info */
204		delim = memchr(line, ',', linelen);
205		if (delim != NULL) {
206			*delim = '\0';
207			loc = delim + 1;
208		} else
209			loc = "";
210
211		/* read each prefix  */
212		if (!geofeed_parse_geoip(geofeed, line, loc))
213			goto out;
214	}
215
216	if (!rpki_signature_seen || !end_signature_seen) {
217		warnx("%s: absent or invalid signature", fn);
218		goto out;
219	}
220
221	if ((base64_decode(b64, strlen(b64), &der, &dersz)) == -1) {
222		warnx("%s: base64_decode failed", fn);
223		goto out;
224	}
225
226	if (!cms_parse_validate_detached(x509, fn, der, dersz, geofeed_oid,
227	    bio, &geofeed->signtime))
228		goto out;
229
230	if (!x509_get_aia(*x509, fn, &geofeed->aia))
231		goto out;
232	if (!x509_get_aki(*x509, fn, &geofeed->aki))
233		goto out;
234	if (!x509_get_ski(*x509, fn, &geofeed->ski))
235		goto out;
236
237	if (geofeed->aia == NULL || geofeed->aki == NULL ||
238	    geofeed->ski == NULL) {
239		warnx("%s: missing AIA, AKI, or SKI X509 extension", fn);
240		goto out;
241	}
242
243	if (!x509_get_notbefore(*x509, fn, &geofeed->notbefore))
244		goto out;
245	if (!x509_get_notafter(*x509, fn, &geofeed->notafter))
246		goto out;
247
248	if ((cert = cert_parse_ee_cert(fn, talid, *x509)) == NULL)
249		goto out;
250
251	if (x509_any_inherits(*x509)) {
252		warnx("%s: inherit elements not allowed in EE cert", fn);
253		goto out;
254	}
255
256	if (cert->asz > 0) {
257		warnx("%s: superfluous AS Resources extension present", fn);
258		goto out;
259	}
260
261	geofeed->valid = valid_geofeed(fn, cert, geofeed);
262
263	rc = 1;
264 out:
265	if (rc == 0) {
266		geofeed_free(geofeed);
267		geofeed = NULL;
268		X509_free(*x509);
269		*x509 = NULL;
270	}
271	cert_free(cert);
272	BIO_free(bio);
273	free(b64);
274	free(der);
275
276	return geofeed;
277}
278
279/*
280 * Free what follows a pointer to a geofeed structure.
281 * Safe to call with NULL.
282 */
283void
284geofeed_free(struct geofeed *p)
285{
286	size_t i;
287
288	if (p == NULL)
289		return;
290
291	for (i = 0; i < p->geoipsz; i++) {
292		free(p->geoips[i].ip);
293		free(p->geoips[i].loc);
294	}
295
296	free(p->geoips);
297	free(p->aia);
298	free(p->aki);
299	free(p->ski);
300	free(p);
301}
302