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