1/* 2 * $OpenBSD: util.c,v 1.5 2023/04/19 12:34:23 jsg Exp $ 3 * Copyright (c) 2002 Institute for Open Systems Technology Australia (IFOST) 4 * Copyright (c) 2007 Michael Erdely <merdely@openbsd.org> 5 * Copyright (c) 2019 Martijn van Duren <martijn@openbsd.org> 6 * 7 * All rights reserved. 8 * 9 * Redistribution and use in source and binary forms, with or without 10 * modification, are permitted provided that the following conditions 11 * are met: 12 * 1. Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 2. Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in the 16 * documentation and/or other materials provided with the distribution. 17 * 3. The name of the author may not be used to endorse or promote products 18 * derived from this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 21 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 22 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 23 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 24 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 25 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 26 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 27 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 28 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 29 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32#include <sys/socket.h> 33#include <sys/time.h> 34#include <sys/types.h> 35#include <sys/stat.h> 36#include <sys/un.h> 37#include <netinet/in.h> 38 39#include <ctype.h> 40#include <grp.h> 41#include <unistd.h> 42#include <stdio.h> 43#include <stdarg.h> 44#include <stdlib.h> 45#include <string.h> 46#include <limits.h> 47#include <errno.h> 48#include <syslog.h> 49#include <tls.h> 50#include <netdb.h> 51#include <login_cap.h> 52 53#include "aldap.h" 54#include "login_ldap.h" 55 56int debug = 0; 57 58static int getscope(char *); 59 60void 61dlog(int d, char *fmt, ...) 62{ 63 va_list ap; 64 65 /* 66 * if debugging is on, print everything to stderr 67 * otherwise, syslog it if d = 0. messing with 68 * newlines means there wont be newlines in stuff 69 * that goes to syslog. 70 */ 71 72 va_start(ap, fmt); 73 if (debug) { 74 vfprintf(stderr, fmt, ap); 75 fputc('\n', stderr); 76 } else if (d == 0) 77 vsyslog(LOG_WARNING, fmt, ap); 78 79 va_end(ap); 80} 81 82const char * 83ldap_resultcode(enum result_code code) 84{ 85#define CODE(_X) case _X:return (#_X) 86 switch (code) { 87 CODE(LDAP_SUCCESS); 88 CODE(LDAP_OPERATIONS_ERROR); 89 CODE(LDAP_PROTOCOL_ERROR); 90 CODE(LDAP_TIMELIMIT_EXCEEDED); 91 CODE(LDAP_SIZELIMIT_EXCEEDED); 92 CODE(LDAP_COMPARE_FALSE); 93 CODE(LDAP_COMPARE_TRUE); 94 CODE(LDAP_STRONG_AUTH_NOT_SUPPORTED); 95 CODE(LDAP_STRONG_AUTH_REQUIRED); 96 CODE(LDAP_REFERRAL); 97 CODE(LDAP_ADMINLIMIT_EXCEEDED); 98 CODE(LDAP_UNAVAILABLE_CRITICAL_EXTENSION); 99 CODE(LDAP_CONFIDENTIALITY_REQUIRED); 100 CODE(LDAP_SASL_BIND_IN_PROGRESS); 101 CODE(LDAP_NO_SUCH_ATTRIBUTE); 102 CODE(LDAP_UNDEFINED_TYPE); 103 CODE(LDAP_INAPPROPRIATE_MATCHING); 104 CODE(LDAP_CONSTRAINT_VIOLATION); 105 CODE(LDAP_TYPE_OR_VALUE_EXISTS); 106 CODE(LDAP_INVALID_SYNTAX); 107 CODE(LDAP_NO_SUCH_OBJECT); 108 CODE(LDAP_ALIAS_PROBLEM); 109 CODE(LDAP_INVALID_DN_SYNTAX); 110 CODE(LDAP_ALIAS_DEREF_PROBLEM); 111 CODE(LDAP_INAPPROPRIATE_AUTH); 112 CODE(LDAP_INVALID_CREDENTIALS); 113 CODE(LDAP_INSUFFICIENT_ACCESS); 114 CODE(LDAP_BUSY); 115 CODE(LDAP_UNAVAILABLE); 116 CODE(LDAP_UNWILLING_TO_PERFORM); 117 CODE(LDAP_LOOP_DETECT); 118 CODE(LDAP_NAMING_VIOLATION); 119 CODE(LDAP_OBJECT_CLASS_VIOLATION); 120 CODE(LDAP_NOT_ALLOWED_ON_NONLEAF); 121 CODE(LDAP_NOT_ALLOWED_ON_RDN); 122 CODE(LDAP_ALREADY_EXISTS); 123 CODE(LDAP_NO_OBJECT_CLASS_MODS); 124 CODE(LDAP_AFFECTS_MULTIPLE_DSAS); 125 CODE(LDAP_OTHER); 126 } 127 128 return ("UNKNOWN_ERROR"); 129}; 130 131 132static int 133parse_server_line(char *buf, struct aldap_url *s) 134{ 135 /** 136 * host=[<protocol>://]<hostname>[:port] 137 * 138 * must have a hostname 139 * protocol can be "ldap", "ldaps", "ldap+tls" or "ldapi" 140 * for ldap and ldap+tls, port defaults to 389 141 * for ldaps, port defaults to 636 142 */ 143 144 if (buf == NULL) { 145 dlog(1, "%s got NULL buf!", __func__); 146 return 0; 147 } 148 149 dlog(1, "parse_server_line buf = %s", buf); 150 151 memset(s, 0, sizeof(*s)); 152 153 if (aldap_parse_url(buf, s) == -1) { 154 dlog(0, "failed to parse host %s", buf); 155 return 0; 156 } 157 158 if (s->protocol == -1) 159 s->protocol = LDAP; 160 if (s->protocol != LDAPI && s->port == 0) { 161 if (s->protocol == LDAPS) 162 s->port = 636; 163 else 164 s->port = 389; 165 } 166 167 return 1; 168} 169 170int 171parse_conf(struct auth_ctx *ctx, const char *path) 172{ 173 FILE *cf; 174 struct stat sb; 175 struct group *grp; 176 struct aldap_urlq *url; 177 char *buf = NULL, *key, *value, *tail; 178 const char *errstr; 179 size_t buflen = 0; 180 ssize_t linelen; 181 182 dlog(1, "Parsing config file '%s'", path); 183 184 if ((cf = fopen(path, "r")) == NULL) { 185 dlog(0, "Can't open config file: %s", strerror(errno)); 186 return 0; 187 } 188 if (fstat(fileno(cf), &sb) == -1) { 189 dlog(0, "Can't stat config file: %s", strerror(errno)); 190 return 0; 191 } 192 if ((grp = getgrnam("auth")) == NULL) { 193 dlog(0, "Can't find group auth"); 194 return 0; 195 } 196 if (sb.st_uid != 0 || 197 sb.st_gid != grp->gr_gid || 198 (sb.st_mode & S_IRWXU) != (S_IRUSR | S_IWUSR) || 199 (sb.st_mode & S_IRWXG) != S_IRGRP || 200 (sb.st_mode & S_IRWXO) != 0) { 201 dlog(0, "Wrong permissions for config file"); 202 return 0; 203 } 204 205 /* We need a default scope */ 206 ctx->gscope = ctx->scope = getscope(NULL); 207 208 while ((linelen = getline(&buf, &buflen, cf)) != -1) { 209 if (buf[linelen - 1] == '\n') 210 buf[linelen -1] = '\0'; 211 /* Allow leading spaces */ 212 for (key = buf; key[0] != '\0' && isspace(key[0]); key++) 213 continue; 214 /* Comment or white lines */ 215 if (key[0] == '#' || key[0] == '\0') 216 continue; 217 if ((tail = value = strchr(key, '=')) == NULL) { 218 dlog(0, "Missing value for option '%s'", key); 219 return 0; 220 } 221 value++; 222 /* Don't fail over trailing key spaces */ 223 for (tail--; isspace(tail[0]); tail--) 224 continue; 225 tail[1] = '\0'; 226 if (strcmp(key, "host") == 0) { 227 if ((url = calloc(1, sizeof(*url))) == NULL) { 228 dlog(0, "Failed to add %s: %s", value, 229 strerror(errno)); 230 continue; 231 } 232 if (parse_server_line(value, &(url->s)) == 0) { 233 free(url); 234 return 0; 235 } 236 TAILQ_INSERT_TAIL(&(ctx->s), url, entries); 237 } else if (strcmp(key, "basedn") == 0) { 238 free(ctx->basedn); 239 if ((ctx->basedn = strdup(value)) == NULL) { 240 dlog(0, "%s", strerror(errno)); 241 return 0; 242 } 243 } else if (strcmp(key, "binddn") == 0) { 244 free(ctx->binddn); 245 if ((ctx->binddn = parse_filter(ctx, value)) == NULL) 246 return 0; 247 } else if (strcmp(key, "bindpw") == 0) { 248 free(ctx->bindpw); 249 if ((ctx->bindpw = strdup(value)) == NULL) { 250 dlog(0, "%s", strerror(errno)); 251 return 0; 252 } 253 } else if (strcmp(key, "timeout") == 0) { 254 ctx->timeout = strtonum(value, 0, INT_MAX, &errstr); 255 if (ctx->timeout == 0 && errstr != NULL) { 256 dlog(0, "timeout %s", errstr); 257 return 0; 258 } 259 } else if (strcmp(key, "filter") == 0) { 260 free(ctx->filter); 261 if ((ctx->filter = parse_filter(ctx, value)) == NULL) 262 return 0; 263 } else if (strcmp(key, "scope") == 0) { 264 if ((ctx->scope = getscope(value)) == -1) 265 return 0; 266 } else if (strcmp(key, "cacert") == 0) { 267 free(ctx->cacert); 268 if ((ctx->cacert = strdup(value)) == NULL) { 269 dlog(0, "%s", strerror(errno)); 270 return 0; 271 } 272 } else if (strcmp(key, "cacertdir") == 0) { 273 free(ctx->cacertdir); 274 if ((ctx->cacertdir = strdup(value)) == NULL) { 275 dlog(0, "%s", strerror(errno)); 276 return 0; 277 } 278 } else if (strcmp(key, "gbasedn") == 0) { 279 free(ctx->gbasedn); 280 if ((ctx->gbasedn = strdup(value)) == NULL) { 281 dlog(0, "%s", strerror(errno)); 282 return 0; 283 } 284 } else if (strcmp(key, "gfilter") == 0) { 285 free(ctx->gfilter); 286 if ((ctx->gfilter = strdup(value)) == NULL) { 287 dlog(0, "%s", strerror(errno)); 288 return 0; 289 } 290 } else if (strcmp(key, "gscope") == 0) { 291 if ((ctx->scope = getscope(value)) == -1) 292 return 0; 293 } else { 294 dlog(0, "Unknown option '%s'", key); 295 return 0; 296 } 297 } 298 if (ferror(cf)) { 299 dlog(0, "Can't read config file: %s", strerror(errno)); 300 return 0; 301 } 302 if (TAILQ_EMPTY(&(ctx->s))) { 303 dlog(0, "Missing host"); 304 return 0; 305 } 306 if (ctx->basedn == NULL && ctx->binddn == NULL) { 307 dlog(0, "Missing basedn or binddn"); 308 return 0; 309 } 310 return 1; 311} 312 313int 314do_conn(struct auth_ctx *ctx, struct aldap_url *url) 315{ 316 struct addrinfo ai, *res, *res0; 317 struct sockaddr_un un; 318 struct aldap_message *m; 319 struct tls_config *tls_config; 320 const char *errstr; 321 char port[6]; 322 int fd, code; 323 324 dlog(1, "host %s, port %d", url->host, url->port); 325 326 if (url->protocol == LDAPI) { 327 memset(&un, 0, sizeof(un)); 328 un.sun_family = AF_UNIX; 329 if (strlcpy(un.sun_path, url->host, 330 sizeof(un.sun_path)) >= sizeof(un.sun_path)) { 331 dlog(0, "socket '%s' too long", url->host); 332 return 0; 333 } 334 if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1 || 335 connect(fd, (struct sockaddr *)&un, sizeof(un)) == -1) { 336 dlog(0, "can't create socket '%s'", url->host); 337 return 0; 338 } 339 } else { 340 memset(&ai, 0, sizeof(ai)); 341 ai.ai_family = AF_UNSPEC; 342 ai.ai_socktype = SOCK_STREAM; 343 ai.ai_protocol = IPPROTO_TCP; 344 (void)snprintf(port, sizeof(port), "%u", url->port); 345 if ((code = getaddrinfo(url->host, port, 346 &ai, &res0)) != 0) { 347 dlog(0, "%s", gai_strerror(code)); 348 return 0; 349 } 350 for (res = res0; res; res = res->ai_next, fd = -1) { 351 if ((fd = socket(res->ai_family, res->ai_socktype, 352 res->ai_protocol)) == -1) 353 continue; 354 355 if (connect(fd, res->ai_addr, res->ai_addrlen) >= 0) 356 break; 357 358 close(fd); 359 } 360 freeaddrinfo(res0); 361 if (fd == -1) 362 return 0; 363 } 364 365 ctx->ld = aldap_init(fd); 366 if (ctx->ld == NULL) { 367 dlog(0, "aldap_open(%s:%hd) failed", url->host, url->port); 368 return 0; 369 } 370 371 dlog(1, "connect success!"); 372 373 if (url->protocol == LDAPTLS) { 374 dlog(1, "starttls!"); 375 if (aldap_req_starttls(ctx->ld) == -1) { 376 dlog(0, "failed to request STARTTLS"); 377 goto fail; 378 } 379 380 if ((m = aldap_parse(ctx->ld)) == NULL) { 381 dlog(0, "failed to parse STARTTLS response"); 382 goto fail; 383 } 384 385 if (ctx->ld->msgid != m->msgid || 386 (code = aldap_get_resultcode(m)) != LDAP_SUCCESS) { 387 dlog(0, "STARTTLS failed: %s(%d)", 388 ldap_resultcode(code), code); 389 aldap_freemsg(m); 390 goto fail; 391 } 392 aldap_freemsg(m); 393 } 394 if (url->protocol == LDAPTLS || url->protocol == LDAPS) { 395 dlog(1, "%s: starting TLS", __func__); 396 397 if ((tls_config = tls_config_new()) == NULL) { 398 dlog(0, "TLS config failed"); 399 goto fail; 400 } 401 402 if (ctx->cacert != NULL && 403 tls_config_set_ca_file(tls_config, ctx->cacert) == -1) { 404 dlog(0, "Failed to set ca file %s", ctx->cacert); 405 goto fail; 406 } 407 if (ctx->cacertdir != NULL && 408 tls_config_set_ca_path(tls_config, ctx->cacertdir) == -1) { 409 dlog(0, "Failed to set ca dir %s", ctx->cacertdir); 410 goto fail; 411 } 412 413 if (aldap_tls(ctx->ld, tls_config, url->host) < 0) { 414 aldap_get_errno(ctx->ld, &errstr); 415 dlog(0, "TLS failed: %s", errstr); 416 goto fail; 417 } 418 } 419 return 1; 420fail: 421 aldap_close(ctx->ld); 422 return 0; 423} 424 425int 426conn(struct auth_ctx *ctx) 427{ 428 struct aldap_urlq *url; 429 430 TAILQ_FOREACH(url, &(ctx->s), entries) { 431 if (do_conn(ctx, &(url->s))) 432 return 1; 433 } 434 435 /* all the urls have failed */ 436 return 0; 437} 438 439static int 440getscope(char *scope) 441{ 442 if (scope == NULL || scope[0] == '\0') 443 return LDAP_SCOPE_SUBTREE; 444 445 if (strcmp(scope, "base") == 0) 446 return LDAP_SCOPE_BASE; 447 else if (strcmp(scope, "one") == 0) 448 return LDAP_SCOPE_ONELEVEL; 449 else if (strcmp(scope, "sub") == 0) 450 return LDAP_SCOPE_SUBTREE; 451 452 dlog(0, "Invalid scope"); 453 return -1; 454} 455 456/* 457 * Convert format specifiers from the filter in login.conf to their 458 * real values. return the new filter in the filter argument. 459 */ 460char * 461parse_filter(struct auth_ctx *ctx, const char *str) 462{ 463 char tmp[PATH_MAX]; 464 char hostname[HOST_NAME_MAX+1]; 465 const char *p; 466 char *q; 467 468 if (str == NULL) 469 return NULL; 470 471 /* 472 * copy over from str to q, if we hit a %, substitute the real value, 473 * if we hit a NULL, its the end of the filter string 474 */ 475 for (p = str, q = tmp; p[0] != '\0' && 476 ((size_t)(q - tmp) < sizeof(tmp)); p++) { 477 if (p[0] == '%') { 478 p++; 479 480 /* Make sure we can find the end of tmp for strlcat */ 481 q[0] = '\0'; 482 483 /* 484 * Don't need to check strcat for truncation, since we 485 * will bail on the next iteration 486 */ 487 switch (p[0]) { 488 case 'u': /* username */ 489 q = tmp + strlcat(tmp, ctx->user, sizeof(tmp)); 490 break; 491 case 'h': /* hostname */ 492 if (gethostname(hostname, sizeof(hostname)) == 493 -1) { 494 dlog(0, "couldn't get host name for " 495 "%%h %s", strerror(errno)); 496 return NULL; 497 } 498 q = tmp + strlcat(tmp, hostname, sizeof(tmp)); 499 break; 500 case 'd': /* user dn */ 501 if (ctx->userdn == NULL) { 502 dlog(0, "no userdn has been recorded"); 503 return 0; 504 } 505 q = tmp + strlcat(tmp, ctx->userdn, 506 sizeof(tmp)); 507 break; 508 case '%': /* literal % */ 509 q[0] = p[0]; 510 q++; 511 break; 512 default: 513 dlog(0, "%s: invalid filter specifier", 514 __func__); 515 return NULL; 516 } 517 } else { 518 q[0] = p[0]; 519 q++; 520 } 521 } 522 if ((size_t) (q - tmp) >= sizeof(tmp)) { 523 dlog(0, "filter string too large, unable to process: %s", str); 524 return NULL; 525 } 526 527 q[0] = '\0'; 528 q = strdup(tmp); 529 if (q == NULL) { 530 dlog(0, "%s", strerror(errno)); 531 return NULL; 532 } 533 534 return q; 535} 536