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