xref: /openbsd/usr.sbin/rpki-client/geofeed.c (revision 3bef86f7)
1 /*	$OpenBSD: geofeed.c,v 1.15 2023/10/13 12:06:49 job 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 
34 struct	parse {
35 	const char	*fn;
36 	struct geofeed	*res;
37 };
38 
39 extern ASN1_OBJECT	*geofeed_oid;
40 
41 /*
42  * Take a CIDR prefix (in presentation format) and add it to parse results.
43  * Returns 1 on success, 0 on failure.
44  */
45 static int
46 geofeed_parse_geoip(struct geofeed *res, char *cidr, char *loc)
47 {
48 	struct geoip	*geoip;
49 	struct ip_addr	*ipaddr;
50 	enum afi	 afi;
51 	int		 plen;
52 
53 	if ((ipaddr = calloc(1, sizeof(struct ip_addr))) == NULL)
54 		err(1, NULL);
55 
56 	if ((plen = inet_net_pton(AF_INET, cidr, ipaddr->addr,
57 	    sizeof(ipaddr->addr))) != -1)
58 		afi = AFI_IPV4;
59 	else if ((plen = inet_net_pton(AF_INET6, cidr, ipaddr->addr,
60 	    sizeof(ipaddr->addr))) != -1)
61 		afi = AFI_IPV6;
62 	else {
63 		static char buf[80];
64 
65 		if (strnvis(buf, cidr, sizeof(buf), VIS_SAFE)
66 		    >= (int)sizeof(buf)) {
67 			memcpy(buf + sizeof(buf) - 4, "...", 4);
68 		}
69 		warnx("invalid address: %s", buf);
70 		free(ipaddr);
71 		return 0;
72 	}
73 
74 	ipaddr->prefixlen = plen;
75 
76 	res->geoips = recallocarray(res->geoips, res->geoipsz,
77 	    res->geoipsz + 1, sizeof(struct geoip));
78 	if (res->geoips == NULL)
79 		err(1, NULL);
80 	geoip = &res->geoips[res->geoipsz++];
81 
82 	if ((geoip->ip = calloc(1, sizeof(struct cert_ip))) == NULL)
83 		err(1, NULL);
84 
85 	geoip->ip->type = CERT_IP_ADDR;
86 	geoip->ip->ip = *ipaddr;
87 	geoip->ip->afi = afi;
88 
89 	if ((geoip->loc = strdup(loc)) == NULL)
90 		err(1, NULL);
91 
92 	if (!ip_cert_compose_ranges(geoip->ip))
93 		return 0;
94 
95 	return 1;
96 }
97 
98 /*
99  * Parse a full RFC 9092 file.
100  * Returns the Geofeed, or NULL if the object was malformed.
101  */
102 struct geofeed *
103 geofeed_parse(X509 **x509, const char *fn, int talid, char *buf, size_t len)
104 {
105 	struct parse	 p;
106 	char		*delim, *line, *loc, *nl;
107 	ssize_t		 linelen;
108 	BIO		*bio;
109 	char		*b64 = NULL;
110 	size_t		 b64sz = 0;
111 	unsigned char	*der = NULL;
112 	size_t		 dersz;
113 	struct cert	*cert = NULL;
114 	int		 rpki_signature_seen = 0, end_signature_seen = 0;
115 	int		 rc = 0;
116 
117 	bio = BIO_new(BIO_s_mem());
118 	if (bio == NULL)
119 		errx(1, "BIO_new");
120 
121 	memset(&p, 0, sizeof(struct parse));
122 	p.fn = fn;
123 
124 	if ((p.res = calloc(1, sizeof(struct geofeed))) == NULL)
125 		err(1, NULL);
126 
127 	while ((nl = memchr(buf, '\n', len)) != NULL) {
128 		line = buf;
129 
130 		/* advance buffer to next line */
131 		len -= nl + 1 - buf;
132 		buf = nl + 1;
133 
134 		/* replace LF and CR with NUL, point nl at first NUL */
135 		*nl = '\0';
136 		if (nl > line && nl[-1] == '\r') {
137 			nl[-1] = '\0';
138 			nl--;
139 			linelen = nl - line;
140 		} else {
141 			warnx("%s: malformed file, expected CRLF line"
142 			    " endings", fn);
143 			goto out;
144 		}
145 
146 		if (end_signature_seen) {
147 			warnx("%s: trailing data after signature section", fn);
148 			goto out;
149 		}
150 
151 		if (rpki_signature_seen) {
152 			if (strncmp(line, "# End Signature:",
153 			    strlen("# End Signature:")) == 0) {
154 				end_signature_seen = 1;
155 				continue;
156 			}
157 
158 			if (linelen > 74) {
159 				warnx("%s: line in signature section too long",
160 				    fn);
161 				goto out;
162 			}
163 			if (strncmp(line, "# ", strlen("# ")) != 0) {
164 				warnx("%s: line in signature section too "
165 				    "short", fn);
166 				goto out;
167 			}
168 
169 			/* skip over "# " */
170 			line += 2;
171 			strlcat(b64, line, b64sz);
172 			continue;
173 		}
174 
175 		if (strncmp(line, "# RPKI Signature:",
176 		    strlen("# RPKI Signature:")) == 0) {
177 			rpki_signature_seen = 1;
178 
179 			if ((b64 = calloc(1, len)) == NULL)
180 				err(1, NULL);
181 			b64sz = len;
182 
183 			continue;
184 		}
185 
186 		/*
187 		 * Read the Geofeed CSV records into a BIO to later on
188 		 * calculate the message digest and compare with the one
189 		 * in the detached CMS signature.
190 		 */
191 		if (BIO_puts(bio, line) != linelen ||
192 		    BIO_puts(bio, "\r\n") != 2) {
193 			warnx("%s: BIO_puts failed", fn);
194 			goto out;
195 		}
196 
197 		/* Zap comments and whitespace before them. */
198 		delim = memchr(line, '#', linelen);
199 		if (delim != NULL) {
200 			while (delim > line &&
201 			    isspace((unsigned char)delim[-1]))
202 				delim--;
203 			*delim = '\0';
204 			linelen = delim - line;
205 		}
206 
207 		/* Skip empty lines. */
208 		if (linelen == 0)
209 			continue;
210 
211 		/* Split prefix and location info */
212 		delim = memchr(line, ',', linelen);
213 		if (delim != NULL) {
214 			*delim = '\0';
215 			loc = delim + 1;
216 		} else
217 			loc = "";
218 
219 		/* read each prefix  */
220 		if (!geofeed_parse_geoip(p.res, line, loc))
221 			goto out;
222 	}
223 
224 	if (!rpki_signature_seen || !end_signature_seen) {
225 		warnx("%s: absent or invalid signature", fn);
226 		goto out;
227 	}
228 
229 	if ((base64_decode(b64, strlen(b64), &der, &dersz)) == -1) {
230 		warnx("%s: base64_decode failed", fn);
231 		goto out;
232 	}
233 
234 	if (!cms_parse_validate_detached(x509, fn, der, dersz, geofeed_oid,
235 	    bio, &p.res->signtime))
236 		goto out;
237 
238 	if (!x509_get_aia(*x509, fn, &p.res->aia))
239 		goto out;
240 	if (!x509_get_aki(*x509, fn, &p.res->aki))
241 		goto out;
242 	if (!x509_get_ski(*x509, fn, &p.res->ski))
243 		goto out;
244 
245 	if (p.res->aia == NULL || p.res->aki == NULL || p.res->ski == NULL) {
246 		warnx("%s: missing AIA, AKI, or SKI X509 extension", fn);
247 		goto out;
248 	}
249 
250 	if (!x509_get_notbefore(*x509, fn, &p.res->notbefore))
251 		goto out;
252 	if (!x509_get_notafter(*x509, fn, &p.res->notafter))
253 		goto out;
254 
255 	if ((cert = cert_parse_ee_cert(fn, talid, *x509)) == NULL)
256 		goto out;
257 
258 	if (x509_any_inherits(*x509)) {
259 		warnx("%s: inherit elements not allowed in EE cert", fn);
260 		goto out;
261 	}
262 
263 	if (cert->asz > 0) {
264 		warnx("%s: superfluous AS Resources extension present", fn);
265 		goto out;
266 	}
267 
268 	p.res->valid = valid_geofeed(fn, cert, p.res);
269 
270 	rc = 1;
271  out:
272 	if (rc == 0) {
273 		geofeed_free(p.res);
274 		p.res = NULL;
275 		X509_free(*x509);
276 		*x509 = NULL;
277 	}
278 	cert_free(cert);
279 	BIO_free(bio);
280 	free(b64);
281 	free(der);
282 
283 	return p.res;
284 }
285 
286 /*
287  * Free what follows a pointer to a geofeed structure.
288  * Safe to call with NULL.
289  */
290 void
291 geofeed_free(struct geofeed *p)
292 {
293 	size_t i;
294 
295 	if (p == NULL)
296 		return;
297 
298 	for (i = 0; i < p->geoipsz; i++) {
299 		free(p->geoips[i].ip);
300 		free(p->geoips[i].loc);
301 	}
302 
303 	free(p->geoips);
304 	free(p->aia);
305 	free(p->aki);
306 	free(p->ski);
307 	free(p);
308 }
309