1 /* $OpenBSD: geofeed.c,v 1.17 2024/11/12 09:23:07 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
34 extern 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 */
40 static int
geofeed_parse_geoip(struct geofeed * geofeed,char * cidr,char * loc)41 geofeed_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->num_geoips,
72 geofeed->num_geoips + 1, sizeof(struct geoip));
73 if (geofeed->geoips == NULL)
74 err(1, NULL);
75 geoip = &geofeed->geoips[geofeed->num_geoips++];
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 */
97 struct geofeed *
geofeed_parse(X509 ** x509,const char * fn,int talid,char * buf,size_t len)98 geofeed_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->num_ases > 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 */
283 void
geofeed_free(struct geofeed * p)284 geofeed_free(struct geofeed *p)
285 {
286 size_t i;
287
288 if (p == NULL)
289 return;
290
291 for (i = 0; i < p->num_geoips; 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