1/*
2 * auth_ldap.m vi:ts=4:sw=4:expandtab:
3 * OpenVPN LDAP Authentication Plugin
4 *
5 * Copyright (c) 2005 - 2007 Landon Fuller <landonf@threerings.net>
6 * Copyright (c) 2007 Three Rings Design, Inc.
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. Neither the name of Landon Fuller nor the names of any contributors
18 *    may be used to endorse or promote products derived from this
19 *    software without specific prior written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
25 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 * POSSIBILITY OF SUCH DAMAGE.
32 */
33
34#import <err.h>
35#import <stdio.h>
36#import <stdlib.h>
37#import <stdarg.h>
38#import <errno.h>
39
40#import <ldap.h>
41
42#import <openvpn-plugin.h>
43
44#import <TRVPNPlugin.h>
45
46#include "openvpn-cr.h"
47
48/* Plugin Context */
49typedef struct ldap_ctx {
50    TRAuthLDAPConfig *config;
51#ifdef HAVE_PF
52    id<TRPacketFilter> pf;
53#endif
54} ldap_ctx;
55
56
57static const char *get_env(const char *key, const char *env[]) {
58    int i;
59
60    if (!env)
61        return (NULL);
62
63    for (i = 0; env[i]; i++) {
64        size_t keylen = strlen(key);
65
66        if (keylen > strlen(env[i]))
67            continue;
68
69        if (!strncmp(key, env[i], keylen)) {
70            const char *p = env[i] + keylen;
71            if (*p == '=')
72                return (p + 1);
73        }
74    }
75
76    return (NULL);
77}
78
79static TRString *quoteForSearch(const char *string) {
80    const char specialChars[] = "*()\\"; /* RFC 2254. We don't care about NULL */
81    TRString *result = [[TRString alloc] init];
82    TRString *unquotedString, *part;
83    TRAutoreleasePool *pool = [[TRAutoreleasePool alloc] init];
84
85    /* Make a copy of the string */
86    unquotedString = [[TRString alloc] initWithCString: string];
87
88    /* Initialize the result */
89    result = [[TRString alloc] init];
90
91    /* Quote all occurrences of the special characters */
92    while ((part = [unquotedString substringToCharset: specialChars]) != NULL) {
93        TRString *temp;
94        size_t index;
95        char c;
96
97        /* Append everything until the first special character */
98        [result appendString: part];
99
100        /* Append the backquote */
101        [result appendCString: "\\"];
102
103        /* Get the special character */
104        index = [unquotedString indexToCharset: specialChars];
105        temp = [unquotedString substringFromIndex: index];
106        c = [temp charAtIndex: 0];
107
108        /* Append it, too! */
109        [result appendChar: c];
110
111        /* Move unquotedString past the special character */
112        temp = [[unquotedString substringFromCharset: specialChars] retain];
113
114        [unquotedString release];
115        unquotedString = temp;
116    }
117
118    /* Append the remainder, if any */
119    if (unquotedString) {
120        [result appendString: unquotedString];
121        [unquotedString release];
122    }
123
124    [pool release];
125
126    return (result);
127}
128
129static TRString *createSearchFilter(TRString *template, const char *username) {
130    TRString *templateString;
131    TRString *result, *part;
132    TRString *quotedName;
133    const char userFormat[] = "%u";
134    TRAutoreleasePool *pool = [[TRAutoreleasePool alloc] init];
135
136    /* Copy the template */
137    templateString = [[[TRString alloc] initWithString: template] autorelease];
138
139    /* Initialize the result */
140    result = [[TRString alloc] init];
141
142    /* Quote the username */
143    quotedName = quoteForSearch(username);
144
145    while ((part = [templateString substringToCString: userFormat]) != NULL) {
146        TRString *temp;
147
148        /* Append everything until the first %u */
149        [result appendString: part];
150
151        /* Append the username */
152        [result appendString: quotedName];
153
154        /* Move templateString past the %u */
155        temp = [templateString substringFromCString: userFormat];
156        templateString = temp;
157    }
158
159    [quotedName release];
160
161    /* Append the remainder, if any */
162    if (templateString) {
163        [result appendString: templateString];
164    }
165
166    [pool release];
167
168    return (result);
169}
170
171#ifdef HAVE_PF
172static BOOL pf_open(struct ldap_ctx *ctx) {
173    TRString *tableName;
174    TRLDAPGroupConfig *groupConfig;
175    TREnumerator *groupIter;
176    pferror_t pferror;
177
178    /* Acquire a reference to /dev/pf */
179    ctx->pf = [[TRLocalPacketFilter alloc] init];
180    if ((pferror = [ctx->pf open]) != PF_SUCCESS) {
181        /* /dev/pf could not be opened. Is it available? */
182        [TRLog error: "Failed to open /dev/pf: %s", [TRPacketFilterUtil stringForError: pferror]];
183        ctx->pf = nil;
184        return NO;
185    }
186
187    /* Clear out all referenced PF tables */
188    if ((tableName = [ctx->config pfTable])) {
189        if ((pferror = [ctx->pf flushTable: tableName]) != PF_SUCCESS) {
190            [TRLog error: "Failed to clear packet filter table \"%s\": %s", [tableName cString], [TRPacketFilterUtil stringForError: pferror]];
191            goto error;
192        }
193    }
194
195    if ([ctx->config ldapGroups]) {
196        groupIter = [[ctx->config ldapGroups] objectEnumerator];
197        while ((groupConfig = [groupIter nextObject]) != nil) {
198            if ((tableName = [groupConfig pfTable])) {
199                if ((pferror = [ctx->pf flushTable: tableName]) != PF_SUCCESS) {
200                    [TRLog error: "Failed to clear packet filter table \"%s\": %s", [tableName cString], [TRPacketFilterUtil stringForError: pferror]];
201                    goto error;
202                }
203            }
204        }
205    }
206
207    return YES;
208
209    error:
210    [ctx->pf release];
211    ctx->pf = NULL;
212    return NO;
213}
214#endif /* HAVE_PF */
215
216OPENVPN_EXPORT openvpn_plugin_handle_t
217openvpn_plugin_open_v1(unsigned int *type, const char *argv[], const char *envp[]) {
218    ldap_ctx *ctx = xmalloc(sizeof(ldap_ctx));
219
220/* Read the configuration */
221    ctx->config = [[TRAuthLDAPConfig alloc] initWithConfigFile: argv[1]];
222    if (!ctx->config) {
223        free(ctx);
224        return (NULL);
225    }
226
227#ifdef HAVE_PF
228    ctx->pf = NULL;
229    /* Open reference to /dev/pf and clear out all of our PF tables */
230    if ([ctx->config pfEnabled] && !pf_open(ctx)) {
231        [ctx->config release];
232        free(ctx);
233        return (NULL);
234    }
235#endif
236
237
238    *type = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY) |
239        OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_CONNECT) |
240        OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_DISCONNECT);
241
242    return (ctx);
243}
244
245OPENVPN_EXPORT void
246    openvpn_plugin_close_v1(openvpn_plugin_handle_t handle)
247{
248    ldap_ctx *ctx = handle;
249
250    if (!ctx)
251        return;
252
253    /* Clean up the configuration file */
254    [ctx->config release];
255
256    /* Clean up PF */
257#ifdef HAVE_PF
258    if (ctx->pf)
259        [ctx->pf release];
260#endif
261
262    /* Finished */
263    free(ctx);
264}
265
266TRLDAPConnection *connect_ldap(TRAuthLDAPConfig *config) {
267    TRLDAPConnection *ldap;
268    TRString *value;
269
270    /* Initialize our LDAP Connection */
271    ldap = [[TRLDAPConnection alloc] initWithURL: [config url] timeout: [config timeout]];
272    if (!ldap) {
273        [TRLog error: "Unable to open LDAP connection to %s\n", [[config url] cString]];
274        return nil;
275    }
276
277    /* Referrals */
278    if ([config referralEnabled]) {
279        if (![ldap setReferralEnabled: YES])
280            goto error;
281    } else {
282        if (![ldap setReferralEnabled: NO])
283            goto error;
284    }
285
286    /* Certificate file */
287    if ((value = [config tlsCACertFile]))
288        if (![ldap setTLSCACertFile: value])
289            goto error;
290
291    /* Certificate directory */
292    if ((value = [config tlsCACertDir]))
293        if (![ldap setTLSCACertDir: value])
294            goto error;
295
296    /* Client Certificate Pair */
297    if ([config tlsCertFile] && [config tlsKeyFile])
298        if(![ldap setTLSClientCert: [config tlsCertFile] keyFile: [config tlsKeyFile]])
299            goto error;
300
301    /* Cipher suite */
302    if ((value = [config tlsCipherSuite]))
303        if(![ldap setTLSCipherSuite: value])
304            goto error;
305
306    /* Start TLS */
307    if ([config tlsEnabled])
308        if (![ldap startTLS])
309            goto error;
310
311    /* Bind if requested */
312    if ([config bindDN]) {
313        if (![ldap bindWithDN: [config bindDN] password: [config bindPassword]]) {
314            [TRLog error: "Unable to bind as %s", [[config bindDN] cString]];
315            goto error;
316        }
317    }
318
319    return ldap;
320
321    error:
322    [ldap release];
323    return nil;
324}
325
326static TRLDAPEntry *find_ldap_user (TRLDAPConnection *ldap, TRAuthLDAPConfig *config, const char *username) {
327    TRString		*searchFilter;
328    TRArray			*ldapEntries;
329    TRLDAPEntry		*result = nil;
330
331    /* Assemble our search filter */
332    searchFilter = createSearchFilter([config searchFilter], username);
333
334    /* Search! */
335    ldapEntries = [ldap searchWithFilter: searchFilter
336        scope: LDAP_SCOPE_SUBTREE
337        baseDN: [config baseDN]
338        attributes: NULL];
339    [searchFilter release];
340    if (!ldapEntries)
341        return nil;
342    if ([ldapEntries count] < 1) {
343        return nil;
344    }
345
346    /* The specified search string may (but should not) return more than one entry.
347     * We ignore any extras. */
348    result = [[ldapEntries lastObject] retain];
349
350    return result;
351}
352
353
354static BOOL auth_ldap_user(TRLDAPConnection *ldap, TRAuthLDAPConfig *config, TRLDAPEntry *ldapUser, const char *password) {
355    TRLDAPConnection *authConn;
356    TRString *passwordString;
357    BOOL result = NO;
358
359    /* Create a second connection for binding */
360    authConn = connect_ldap(config);
361    if (!authConn) {
362        return NO;
363    }
364
365    /* Allocate the string to pass to bindWithDN */
366    passwordString = [[TRString alloc] initWithCString: password];
367
368    if ([authConn bindWithDN: [ldapUser dn] password: passwordString]) {
369        result = YES;
370    }
371
372    [passwordString release];
373    [authConn release];
374
375    return result;
376}
377
378static TRLDAPGroupConfig *find_ldap_group(TRLDAPConnection *ldap, TRAuthLDAPConfig *config, TRLDAPEntry *ldapUser) {
379    TREnumerator *groupIter;
380    TRLDAPGroupConfig *groupConfig;
381    TRArray *ldapEntries;
382    TREnumerator *entryIter;
383    TRLDAPEntry *entry;
384    TRLDAPGroupConfig *result = nil;
385    int userNameLength;
386
387    /*
388     * Groups are loaded into the array in the order that they are listed
389     * in the configuration file, and we are expected to perform
390     * "first match". Thusly, we'll walk the stack from the bottom up.
391     */
392    groupIter = [[config ldapGroups] objectReverseEnumerator];
393
394    while ((groupConfig = [groupIter nextObject]) != nil) {
395
396        /* Search for the group */
397        ldapEntries = [ldap searchWithFilter: [groupConfig searchFilter]
398            scope: LDAP_SCOPE_SUBTREE
399            baseDN: [groupConfig baseDN]
400            attributes: NULL];
401
402        /* Error occured, all stop */
403        if (!ldapEntries)
404            break;
405
406        /* If RFC2307BIS flag is true, search for full DN, otherwise just search for uid */
407        TRString *searchValue = [groupConfig memberRFC2307BIS] ? [ldapUser dn] : [ldapUser rdn];
408
409        /* This will be used if we're using the "search" operation instead of the "compare" operation */
410        TRString *searchFilter = [TRString stringWithFormat: "(%s=%s)", [[groupConfig memberAttribute] cString], [searchValue cString]];
411
412        /* Iterate over the returned entries */
413        entryIter = [ldapEntries objectEnumerator];
414        while ((entry = [entryIter nextObject]) != nil) {
415            if ((![groupConfig useCompareOperation] && [ldap searchWithFilter: searchFilter scope: LDAP_SCOPE_SUBTREE baseDN: [entry dn] attributes: NULL]) ||
416                ([groupConfig useCompareOperation] && [ldap compareDN: [entry dn] withAttribute: [groupConfig memberAttribute] value: searchValue])) {
417                /* Group match! */
418                result = groupConfig;
419            }
420        }
421
422        if (result)
423            break;
424    }
425
426    return result;
427}
428
429/** Handle user authentication. */
430static int handle_auth_user_pass_verify(ldap_ctx *ctx, TRLDAPConnection *ldap, TRLDAPEntry *ldapUser, const char *password) {
431    TRLDAPGroupConfig *groupConfig;
432
433	const char *auth_password = password;
434	if ([ctx->config passWordIsCR]) {
435		openvpn_response resp;
436		char *parse_error;
437		if (!extract_openvpn_cr(password, &resp, &parse_error)) {
438	        [TRLog error: "Error extracting challenge/response from password. Parse error = '%s'", 	parse_error];
439	        return (OPENVPN_PLUGIN_FUNC_ERROR);
440		}
441		auth_password = (const char*)resp.password;
442	}
443
444    /* Authenticate the user */
445    if (!auth_ldap_user(ldap, ctx->config, ldapUser, auth_password)) {
446        [TRLog error: "Incorrect password supplied for LDAP DN \"%s\".", [[ldapUser dn] cString]];
447        return (OPENVPN_PLUGIN_FUNC_ERROR);
448    }
449
450    /* User authenticated, find group, if any */
451    if ([ctx->config ldapGroups]) {
452        groupConfig = find_ldap_group(ldap, ctx->config, ldapUser);
453        if (!groupConfig && [ctx->config requireGroup]) {
454            /* No group match, and group membership is required */
455            return OPENVPN_PLUGIN_FUNC_ERROR;
456        } else {
457            /* Group match! */
458            return OPENVPN_PLUGIN_FUNC_SUCCESS;
459        }
460    } else {
461        // No groups, user OK
462        return OPENVPN_PLUGIN_FUNC_SUCCESS;
463    }
464
465    /* Never reached */
466    return OPENVPN_PLUGIN_FUNC_ERROR;
467}
468
469#ifdef HAVE_PF
470/* Add (or remove) the remote address */
471static BOOL pf_client_connect_disconnect(struct ldap_ctx *ctx, TRString *tableName, const char *remoteAddress, BOOL connecting) {
472    TRString *addressString;
473    TRPFAddress *address;
474    pferror_t pferror;
475
476    addressString = [[TRString alloc] initWithCString: remoteAddress];
477    address = [[TRPFAddress alloc] initWithPresentationAddress: addressString];
478    [addressString release];
479    if (connecting) {
480        [TRLog debug: "Adding address \"%s\" to packet filter table \"%s\".", remoteAddress, [tableName cString]];
481
482        if ((pferror = [ctx->pf addAddress: address toTable: tableName]) != PF_SUCCESS) {
483            [TRLog error: "Failed to add address \"%s\" to table \"%s\": %s", remoteAddress, [tableName cString], [TRPacketFilterUtil stringForError: pferror]];
484            [address release];
485            return NO;
486        }
487    } else {
488        [TRLog debug: "Removing address \"%s\" from packet filter table \"%s\".", remoteAddress, [tableName cString]];
489        if ((pferror = [ctx->pf deleteAddress: address fromTable: tableName]) != PF_SUCCESS) {
490            [TRLog error: "Failed to remove address \"%s\" from table \"%s\": %s",
491                remoteAddress, [tableName cString], [TRPacketFilterUtil stringForError: pferror]];
492            [address release];
493            return NO;
494        }
495    }
496    [address release];
497
498    return YES;
499}
500#endif /* HAVE_PF */
501
502
503/** Handle both connection and disconnection events. */
504static int handle_client_connect_disconnect(ldap_ctx *ctx, TRLDAPConnection *ldap, TRLDAPEntry *ldapUser, const char *remoteAddress, BOOL connecting) {
505    TRLDAPGroupConfig *groupConfig = nil;
506#ifdef HAVE_PF
507    TRString *tableName = nil;
508#endif
509
510    /* Locate the group (config), if any */
511    if ([ctx->config ldapGroups]) {
512        groupConfig = find_ldap_group(ldap, ctx->config, ldapUser);
513        if (!groupConfig && [ctx->config requireGroup]) {
514            [TRLog error: "No matching LDAP group found for user DN \"%s\", and group membership is required.", [[ldapUser dn] cString]];
515            /* No group match, and group membership is required */
516            return OPENVPN_PLUGIN_FUNC_ERROR;
517        }
518    }
519
520#ifdef HAVE_PF
521    /* Grab the requested PF table name, if any */
522    if (groupConfig) {
523        tableName = [groupConfig pfTable];
524    } else {
525        tableName = [ctx->config pfTable];
526    }
527
528    if (tableName)
529        if (!pf_client_connect_disconnect(ctx, tableName, remoteAddress, connecting))
530        return OPENVPN_PLUGIN_FUNC_ERROR;
531#endif /* HAVE_PF */
532
533    return OPENVPN_PLUGIN_FUNC_SUCCESS;
534}
535
536
537
538OPENVPN_EXPORT int
539openvpn_plugin_func_v1(openvpn_plugin_handle_t handle, const int type, const char *argv[], const char *envp[]) {
540    const char *username, *password, *remoteAddress;
541    ldap_ctx *ctx = handle;
542    TRLDAPConnection *ldap = nil;
543    TRLDAPEntry *ldapUser = nil;
544    TRAutoreleasePool *pool = nil;
545    int ret = OPENVPN_PLUGIN_FUNC_ERROR;
546
547    /* Per-request allocation pool. */
548    pool = [[TRAutoreleasePool alloc] init];
549
550    username = get_env("username", envp);
551    TRString *userName=[[TRString alloc]initWithCString: username];
552    password = get_env("password", envp);
553    remoteAddress = get_env("ifconfig_pool_remote_ip", envp);
554
555
556    /* At the very least, we need a username to work with */
557    if (!username) {
558        [TRLog debug: "No remote username supplied to OpenVPN LDAP Plugin."];
559        goto cleanup;
560    }
561
562    /* Create an LDAP connection */
563    if (!(ldap = connect_ldap(ctx->config))) {
564        [TRLog error: "LDAP connect failed."];
565        goto cleanup;
566    }
567
568    /* Find the user record */
569    ldapUser = find_ldap_user(ldap, ctx->config, username);
570    [ldapUser setRDN: userName];
571    if (!ldapUser) {
572        /* No such user. */
573        [TRLog warning: "LDAP user \"%s\" was not found.", username];
574        goto cleanup;
575    }
576
577    switch (type) {
578        /* Password Authentication */
579        case OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY:
580            if (!password) {
581                [TRLog debug: "No remote password supplied to OpenVPN LDAP Plugin (OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY)."];
582            } else {
583                ret = handle_auth_user_pass_verify(ctx, ldap, ldapUser, password);
584            }
585            break;
586        /* New connection established */
587        case OPENVPN_PLUGIN_CLIENT_CONNECT:
588            if (!remoteAddress) {
589                [TRLog debug: "No remote address supplied to OpenVPN LDAP Plugin (OPENVPN_PLUGIN_CLIENT_CONNECT)."];
590            } else {
591                ret = handle_client_connect_disconnect(ctx, ldap, ldapUser, remoteAddress, YES);
592            }
593            break;
594        case OPENVPN_PLUGIN_CLIENT_DISCONNECT:
595            if (!remoteAddress) {
596                [TRLog debug: "No remote address supplied to OpenVPN LDAP Plugin (OPENVPN_PLUGIN_CLIENT_DISCONNECT)."];
597            } else {
598                ret = handle_client_connect_disconnect(ctx, ldap, ldapUser, remoteAddress, NO);
599            }
600            break;
601        default:
602            [TRLog debug: "Unhandled plugin type in OpenVPN LDAP Plugin (type=%d)", type];
603            break;
604    }
605
606cleanup:
607    if (ldapUser != nil)
608        [ldapUser release];
609
610    if (ldap != nil)
611        [ldap release];
612
613    if (pool != nil)
614        [pool release];
615
616    return (ret);
617}
618