1/* $OpenBSD: constraints.c,v 1.4 2024/03/15 05:14:16 tb Exp $ */ 2/* 3 * Copyright (c) 2023 Job Snijders <job@openbsd.org> 4 * Copyright (c) 2023 Theo Buehler <tb@openbsd.org> 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 <errno.h> 26#include <fcntl.h> 27#include <libgen.h> 28#include <stdint.h> 29#include <stdio.h> 30#include <stdlib.h> 31#include <string.h> 32#include <unistd.h> 33 34#include <openssl/asn1.h> 35#include <openssl/x509v3.h> 36 37#include "extern.h" 38 39struct tal_constraints { 40 int fd; /* constraints file descriptor or -1. */ 41 char *fn; /* constraints filename */ 42 char *warn; /* warning msg used for violations */ 43 struct cert_ip *allow_ips; /* list of allowed IP address ranges */ 44 size_t allow_ipsz; /* length of "allow_ips" */ 45 struct cert_as *allow_as; /* allowed AS numbers and ranges */ 46 size_t allow_asz; /* length of "allow_as" */ 47 struct cert_ip *deny_ips; /* forbidden IP address ranges */ 48 size_t deny_ipsz; /* length of "deny_ips" */ 49 struct cert_as *deny_as; /* forbidden AS numbers and ranges */ 50 size_t deny_asz; /* length of "deny_as" */ 51} tal_constraints[TALSZ_MAX]; 52 53/* 54 * If there is a .constraints file next to a .tal file, load its contents 55 * into into tal_constraints[talid]. The load function only opens the fd 56 * and stores the filename. The actual parsing happens in constraints_parse(). 57 * Resources of EE certs can then be constrained using constraints_validate(). 58 */ 59 60static void 61constraints_load_talid(int talid) 62{ 63 const char *tal = tals[talid]; 64 char *constraints = NULL, *warning = NULL, *cbn; 65 int fd; 66 size_t len; 67 int saved_errno; 68 69 tal_constraints[talid].fd = -1; 70 71 if (rtype_from_file_extension(tal) != RTYPE_TAL) 72 return; 73 74 /* Replace .tal suffix with .constraints. */ 75 len = strlen(tal) - 4; 76 if (asprintf(&constraints, "%.*s.constraints", (int)len, tal) == -1) 77 err(1, NULL); 78 79 /* prepare warning message for when violations are detected */ 80 if ((cbn = basename(constraints)) == NULL) 81 err(1, "basename"); 82 if (asprintf(&warning, "resource violates %s", cbn) == -1) 83 err(1, NULL); 84 85 saved_errno = errno; 86 87 fd = open(constraints, O_RDONLY); 88 if (fd == -1 && errno != ENOENT) 89 err(1, "failed to load constraints for %s", tal); 90 91 tal_constraints[talid].fn = constraints; 92 tal_constraints[talid].fd = fd; 93 tal_constraints[talid].warn = warning; 94 95 errno = saved_errno; 96} 97 98/* 99 * Iterate over all TALs and load the corresponding constraints files. 100 */ 101void 102constraints_load(void) 103{ 104 int talid; 105 106 for (talid = 0; talid < talsz; talid++) 107 constraints_load_talid(talid); 108} 109 110void 111constraints_unload(void) 112{ 113 int saved_errno, talid; 114 115 saved_errno = errno; 116 for (talid = 0; talid < talsz; talid++) { 117 if (tal_constraints[talid].fd != -1) 118 close(tal_constraints[talid].fd); 119 free(tal_constraints[talid].fn); 120 free(tal_constraints[talid].warn); 121 tal_constraints[talid].fd = -1; 122 tal_constraints[talid].fn = NULL; 123 tal_constraints[talid].warn = NULL; 124 } 125 errno = saved_errno; 126} 127 128/* 129 * Split a string at '-' and trim whitespace around the '-'. 130 * Assumes leading and trailing whitespace in p has already been trimmed. 131 */ 132static int 133constraints_split_range(char *p, const char **min, const char **max) 134{ 135 char *pp; 136 137 *min = p; 138 if ((*max = pp = strchr(p, '-')) == NULL) 139 return 0; 140 141 /* Trim whitespace before '-'. */ 142 while (pp > *min && isspace((unsigned char)pp[-1])) 143 pp--; 144 *pp = '\0'; 145 146 /* Skip past '-' and whitespace following it. */ 147 (*max)++; 148 while (isspace((unsigned char)**max)) 149 (*max)++; 150 151 return 1; 152} 153 154/* 155 * Helper functions to parse textual representations of IP prefixes or ranges. 156 * The RFC 3779 API has poor error reporting, so as a debugging aid, we call 157 * the prohibitively expensive X509v3_addr_canonize() in high verbosity mode. 158 */ 159 160static void 161constraints_parse_ip_prefix(const char *fn, const char *prefix, enum afi afi, 162 IPAddrBlocks *addrs) 163{ 164 unsigned char addr[16] = { 0 }; 165 int af = afi == AFI_IPV4 ? AF_INET : AF_INET6; 166 int plen; 167 168 if ((plen = inet_net_pton(af, prefix, addr, sizeof(addr))) == -1) 169 errx(1, "%s: failed to parse %s", fn, prefix); 170 171 if (!X509v3_addr_add_prefix(addrs, afi, NULL, addr, plen)) 172 errx(1, "%s: failed to add prefix %s", fn, prefix); 173 174 if (verbose < 3) 175 return; 176 177 if (!X509v3_addr_canonize(addrs)) 178 errx(1, "%s: failed to canonize with prefix %s", fn, prefix); 179} 180 181static void 182constraints_parse_ip_range(const char *fn, const char *min, const char *max, 183 enum afi afi, IPAddrBlocks *addrs) 184{ 185 unsigned char min_addr[16] = {0}, max_addr[16] = {0}; 186 int af = afi == AFI_IPV4 ? AF_INET : AF_INET6; 187 188 if (inet_pton(af, min, min_addr) != 1) 189 errx(1, "%s: failed to parse %s", fn, min); 190 if (inet_pton(af, max, max_addr) != 1) 191 errx(1, "%s: failed to parse %s", fn, max); 192 193 if (!X509v3_addr_add_range(addrs, afi, NULL, min_addr, max_addr)) 194 errx(1, "%s: failed to add range %s--%s", fn, min, max); 195 196 if (verbose < 3) 197 return; 198 199 if (!X509v3_addr_canonize(addrs)) 200 errx(1, "%s: failed to canonize with range %s--%s", fn, 201 min, max); 202} 203 204static void 205constraints_parse_ip(const char *fn, char *p, enum afi afi, IPAddrBlocks *addrs) 206{ 207 const char *min, *max; 208 209 if (strchr(p, '-') == NULL) { 210 constraints_parse_ip_prefix(fn, p, afi, addrs); 211 return; 212 } 213 214 if (!constraints_split_range(p, &min, &max)) 215 errx(1, "%s: failed to split range: %s", fn, p); 216 217 constraints_parse_ip_range(fn, min, max, afi, addrs); 218} 219 220/* 221 * Helper functions to parse textual representations of AS numbers or ranges. 222 * The RFC 3779 API has poor error reporting, so as a debugging aid, we call 223 * the prohibitively expensive X509v3_asid_canonize() in high verbosity mode. 224 */ 225 226static void 227constraints_parse_asn(const char *fn, const char *asn, ASIdentifiers *asids) 228{ 229 ASN1_INTEGER *id; 230 231 if ((id = s2i_ASN1_INTEGER(NULL, asn)) == NULL) 232 errx(1, "%s: failed to parse AS %s", fn, asn); 233 234 if (!X509v3_asid_add_id_or_range(asids, V3_ASID_ASNUM, id, NULL)) 235 errx(1, "%s: failed to add AS %s", fn, asn); 236 237 if (verbose < 3) 238 return; 239 240 if (!X509v3_asid_canonize(asids)) 241 errx(1, "%s: failed to canonize with AS %s", fn, asn); 242} 243 244static void 245constraints_parse_asn_range(const char *fn, const char *min, const char *max, 246 ASIdentifiers *asids) 247{ 248 ASN1_INTEGER *min_as, *max_as; 249 250 if ((min_as = s2i_ASN1_INTEGER(NULL, min)) == NULL) 251 errx(1, "%s: failed to parse AS %s", fn, min); 252 if ((max_as = s2i_ASN1_INTEGER(NULL, max)) == NULL) 253 errx(1, "%s: failed to parse AS %s", fn, max); 254 255 if (!X509v3_asid_add_id_or_range(asids, V3_ASID_ASNUM, min_as, max_as)) 256 errx(1, "%s: failed to add AS range %s--%s", fn, min, max); 257 258 if (verbose < 3) 259 return; 260 261 if (!X509v3_asid_canonize(asids)) 262 errx(1, "%s: failed to canonize with AS range %s--%s", fn, 263 min, max); 264} 265 266static void 267constraints_parse_as(const char *fn, char *p, ASIdentifiers *asids) 268{ 269 const char *min, *max; 270 271 if (strchr(p, '-') == NULL) { 272 constraints_parse_asn(fn, p, asids); 273 return; 274 } 275 276 if (!constraints_split_range(p, &min, &max)) 277 errx(1, "%s: failed to split range: %s", fn, p); 278 279 constraints_parse_asn_range(fn, min, max, asids); 280} 281 282/* 283 * Work around an annoying bug in X509v3_addr_add_range(). The upper bound 284 * of a range can have unused bits set in its ASN1_BIT_STRING representation. 285 * This triggers a check in ip_addr_parse(). A round trip through DER fixes 286 * this mess up. For extra special fun, {d2i,i2d}_IPAddrBlocks() isn't part 287 * of the API and implementing them for OpenSSL 3 is hairy, so do the round 288 * tripping once per address family. 289 */ 290static void 291constraints_normalize_ip_addrblocks(const char *fn, IPAddrBlocks **addrs) 292{ 293 IPAddrBlocks *new_addrs; 294 IPAddressFamily *af; 295 const unsigned char *p; 296 unsigned char *der; 297 int der_len, i; 298 299 if ((new_addrs = IPAddrBlocks_new()) == NULL) 300 err(1, NULL); 301 302 for (i = 0; i < sk_IPAddressFamily_num(*addrs); i++) { 303 af = sk_IPAddressFamily_value(*addrs, i); 304 305 der = NULL; 306 if ((der_len = i2d_IPAddressFamily(af, &der)) <= 0) 307 errx(1, "%s: failed to convert to DER", fn); 308 p = der; 309 if ((af = d2i_IPAddressFamily(NULL, &p, der_len)) == NULL) 310 errx(1, "%s: failed to convert from DER", fn); 311 free(der); 312 313 if (!sk_IPAddressFamily_push(new_addrs, af)) 314 errx(1, "%s: failed to push constraints", fn); 315 } 316 317 IPAddrBlocks_free(*addrs); 318 *addrs = new_addrs; 319} 320 321/* 322 * If there is a constraints file for tals[talid], load it into a buffer 323 * and parse it line by line. Leverage the above parse helpers to build up 324 * IPAddrBlocks and ASIdentifiers. We use the RFC 3779 API to benefit from 325 * the limited abilities of X509v3_{addr,asid}_canonize() to sort and merge 326 * adjacent ranges. This doesn't deal with overlaps or duplicates, but it's 327 * better than nothing. 328 */ 329 330static void 331constraints_parse_talid(int talid) 332{ 333 IPAddrBlocks *allow_addrs, *deny_addrs; 334 ASIdentifiers *allow_asids, *deny_asids; 335 FILE *f; 336 char *fn, *p, *pp; 337 struct cert_as *allow_as = NULL, *deny_as = NULL; 338 struct cert_ip *allow_ips = NULL, *deny_ips = NULL; 339 size_t allow_asz = 0, allow_ipsz = 0, 340 deny_asz = 0, deny_ipsz = 0; 341 char *line = NULL; 342 size_t len = 0; 343 ssize_t n; 344 int fd, have_allow_as = 0, have_allow_ips = 0, 345 have_deny_as = 0, have_deny_ips = 0; 346 347 fd = tal_constraints[talid].fd; 348 fn = tal_constraints[talid].fn; 349 tal_constraints[talid].fd = -1; 350 tal_constraints[talid].fn = NULL; 351 352 if (fd == -1) { 353 free(fn); 354 return; 355 } 356 357 if ((f = fdopen(fd, "r")) == NULL) 358 err(1, "fdopen"); 359 360 if ((allow_addrs = IPAddrBlocks_new()) == NULL) 361 err(1, NULL); 362 if ((allow_asids = ASIdentifiers_new()) == NULL) 363 err(1, NULL); 364 if ((deny_addrs = IPAddrBlocks_new()) == NULL) 365 err(1, NULL); 366 if ((deny_asids = ASIdentifiers_new()) == NULL) 367 err(1, NULL); 368 369 while ((n = getline(&line, &len, f)) != -1) { 370 if (line[n - 1] == '\n') 371 line[n - 1] = '\0'; 372 373 p = line; 374 375 /* Zap leading whitespace */ 376 while (isspace((unsigned char)*p)) 377 p++; 378 379 /* Zap comments */ 380 if ((pp = strchr(p, '#')) != NULL) 381 *pp = '\0'; 382 383 /* Zap trailing whitespace */ 384 if (pp == NULL) 385 pp = p + strlen(p); 386 while (pp > p && isspace((unsigned char)pp[-1])) 387 pp--; 388 *pp = '\0'; 389 390 if (strlen(p) == 0) 391 continue; 392 393 if (strncmp(p, "allow", strlen("allow")) == 0) { 394 p += strlen("allow"); 395 396 /* Ensure there's whitespace and jump over it. */ 397 if (!isspace((unsigned char)*p)) 398 errx(1, "%s: failed to parse %s", fn, p); 399 while (isspace((unsigned char)*p)) 400 p++; 401 402 if (strchr(p, '.') != NULL) { 403 constraints_parse_ip(fn, p, AFI_IPV4, 404 allow_addrs); 405 have_allow_ips = 1; 406 } else if (strchr(p, ':') != NULL) { 407 constraints_parse_ip(fn, p, AFI_IPV6, 408 allow_addrs); 409 have_allow_ips = 1; 410 } else { 411 constraints_parse_as(fn, p, allow_asids); 412 have_allow_as = 1; 413 } 414 } else if (strncmp(p, "deny", strlen("deny")) == 0) { 415 p += strlen("deny"); 416 417 /* Ensure there's whitespace and jump over it. */ 418 if (!isspace((unsigned char)*p)) 419 errx(1, "%s: failed to parse %s", fn, p); 420 /* Zap leading whitespace */ 421 while (isspace((unsigned char)*p)) 422 p++; 423 424 if (strchr(p, '.') != NULL) { 425 constraints_parse_ip(fn, p, AFI_IPV4, 426 deny_addrs); 427 have_deny_ips = 1; 428 } else if (strchr(p, ':') != NULL) { 429 constraints_parse_ip(fn, p, AFI_IPV6, 430 deny_addrs); 431 have_deny_ips = 1; 432 } else { 433 constraints_parse_as(fn, p, deny_asids); 434 have_deny_as = 1; 435 } 436 } else 437 errx(1, "%s: failed to parse %s", fn, p); 438 } 439 free(line); 440 441 if (ferror(f)) 442 err(1, "%s", fn); 443 fclose(f); 444 445 if (!X509v3_addr_canonize(allow_addrs)) 446 errx(1, "%s: failed to canonize IP addresses allowlist", fn); 447 if (!X509v3_asid_canonize(allow_asids)) 448 errx(1, "%s: failed to canonize AS numbers allowlist", fn); 449 if (!X509v3_addr_canonize(deny_addrs)) 450 errx(1, "%s: failed to canonize IP addresses denylist", fn); 451 if (!X509v3_asid_canonize(deny_asids)) 452 errx(1, "%s: failed to canonize AS numbers denylist", fn); 453 454 if (have_allow_as) { 455 if (!sbgp_parse_assysnum(fn, allow_asids, &allow_as, 456 &allow_asz)) 457 errx(1, "%s: failed to parse AS identifiers allowlist", 458 fn); 459 } 460 if (have_deny_as) { 461 if (!sbgp_parse_assysnum(fn, deny_asids, &deny_as, 462 &deny_asz)) 463 errx(1, "%s: failed to parse AS identifiers denylist", 464 fn); 465 } 466 if (have_allow_ips) { 467 constraints_normalize_ip_addrblocks(fn, &allow_addrs); 468 469 if (!sbgp_parse_ipaddrblk(fn, allow_addrs, &allow_ips, 470 &allow_ipsz)) 471 errx(1, "%s: failed to parse IP addresses allowlist", 472 fn); 473 } 474 if (have_deny_ips) { 475 constraints_normalize_ip_addrblocks(fn, &deny_addrs); 476 477 if (!sbgp_parse_ipaddrblk(fn, deny_addrs, &deny_ips, 478 &deny_ipsz)) 479 errx(1, "%s: failed to parse IP addresses denylist", 480 fn); 481 } 482 483 tal_constraints[talid].allow_as = allow_as; 484 tal_constraints[talid].allow_asz = allow_asz; 485 tal_constraints[talid].allow_ips = allow_ips; 486 tal_constraints[talid].allow_ipsz = allow_ipsz; 487 tal_constraints[talid].deny_as = deny_as; 488 tal_constraints[talid].deny_asz = deny_asz; 489 tal_constraints[talid].deny_ips = deny_ips; 490 tal_constraints[talid].deny_ipsz = deny_ipsz; 491 492 IPAddrBlocks_free(allow_addrs); 493 IPAddrBlocks_free(deny_addrs); 494 ASIdentifiers_free(allow_asids); 495 ASIdentifiers_free(deny_asids); 496 497 free(fn); 498} 499 500/* 501 * Iterate over all TALs and parse the constraints files loaded previously. 502 */ 503void 504constraints_parse(void) 505{ 506 int talid; 507 508 for (talid = 0; talid < talsz; talid++) 509 constraints_parse_talid(talid); 510} 511 512static int 513constraints_check_as(const char *fn, struct cert_as *cert, 514 const struct cert_as *allow_as, size_t allow_asz, 515 const struct cert_as *deny_as, size_t deny_asz) 516{ 517 uint32_t min, max; 518 519 /* Inheriting EE resources are not to be constrained. */ 520 if (cert->type == CERT_AS_INHERIT) 521 return 1; 522 523 if (cert->type == CERT_AS_ID) { 524 min = cert->id; 525 max = cert->id; 526 } else { 527 min = cert->range.min; 528 max = cert->range.max; 529 } 530 531 if (deny_as != NULL) { 532 if (!as_check_overlap(cert, fn, deny_as, deny_asz, 1)) 533 return 0; 534 } 535 if (allow_as != NULL) { 536 if (as_check_covered(min, max, allow_as, allow_asz) <= 0) 537 return 0; 538 } 539 return 1; 540} 541 542static int 543constraints_check_ips(const char *fn, struct cert_ip *cert, 544 const struct cert_ip *allow_ips, size_t allow_ipsz, 545 const struct cert_ip *deny_ips, size_t deny_ipsz) 546{ 547 /* Inheriting EE resources are not to be constrained. */ 548 if (cert->type == CERT_IP_INHERIT) 549 return 1; 550 551 if (deny_ips != NULL) { 552 if (!ip_addr_check_overlap(cert, fn, deny_ips, deny_ipsz, 1)) 553 return 0; 554 } 555 if (allow_ips != NULL) { 556 if (ip_addr_check_covered(cert->afi, cert->min, cert->max, 557 allow_ips, allow_ipsz) <= 0) 558 return 0; 559 } 560 return 1; 561} 562 563/* 564 * Check whether an EE cert's resources are covered by its TAL's constraints. 565 * We accept certs with a negative talid as "unknown TAL" for filemode. The 566 * logic nearly duplicates valid_cert(). 567 */ 568int 569constraints_validate(const char *fn, const struct cert *cert) 570{ 571 int talid = cert->talid; 572 struct cert_as *allow_as, *deny_as; 573 struct cert_ip *allow_ips, *deny_ips; 574 size_t i, allow_asz, allow_ipsz, deny_asz, deny_ipsz; 575 576 /* Accept negative talid to bypass validation. */ 577 if (talid < 0) 578 return 1; 579 if (talid >= talsz) 580 errx(1, "%s: talid out of range %d", fn, talid); 581 582 allow_as = tal_constraints[talid].allow_as; 583 allow_asz = tal_constraints[talid].allow_asz; 584 deny_as = tal_constraints[talid].deny_as; 585 deny_asz = tal_constraints[talid].deny_asz; 586 587 for (i = 0; i < cert->asz; i++) { 588 if (constraints_check_as(fn, &cert->as[i], allow_as, allow_asz, 589 deny_as, deny_asz)) 590 continue; 591 592 as_warn(fn, tal_constraints[talid].warn, &cert->as[i]); 593 return 0; 594 } 595 596 allow_ips = tal_constraints[talid].allow_ips; 597 allow_ipsz = tal_constraints[talid].allow_ipsz; 598 deny_ips = tal_constraints[talid].deny_ips; 599 deny_ipsz = tal_constraints[talid].deny_ipsz; 600 601 for (i = 0; i < cert->ipsz; i++) { 602 if (constraints_check_ips(fn, &cert->ips[i], allow_ips, 603 allow_ipsz, deny_ips, deny_ipsz)) 604 continue; 605 606 ip_warn(fn, tal_constraints[talid].warn, &cert->ips[i]); 607 return 0; 608 } 609 610 return 1; 611} 612