2 * Copyright (c) 2021, PADL Software Pty Ltd.
5 * Portions Copyright (c) 2004 Kungliga Tekniska Högskolan
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
11 * 1. Redistributions of source code must retain the above copyright
12 * 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.
18 * 3. Neither the name of PADL Software nor the names of its contributors
19 * may be used to endorse or promote products derived from this software
20 * without specific prior written permission.
22 * THIS SOFTWARE IS PROVIDED BY PADL SOFTWARE AND CONTRIBUTORS ``AS IS'' AND
23 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25 * ARE DISCLAIMED. IN NO EVENT SHALL PADL SOFTWARE OR CONTRIBUTORS BE LIABLE
26 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
28 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
29 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
31 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
36 * This plugin authorizes federated GSS-API pre-authentication clients by
37 * querying an AD DC in the KDC realm for the altSecurityIdentities
40 * For example, GSS-API initiator foo@AAA.H5L.SE using the eap-aes128
41 * mechanism to authenticate in realm H5L.SE would require a user entry
42 * where altSecurityIdentities equals either:
45 * EAP-AES128:foo@AAA.H5L.SE
47 * (Stripping the mechanism name after the hyphen is a convention
48 * intended to allow mechanism variants to be grouped together.)
50 * Once the user entry is found, the name is canonicalized by reading the
51 * sAMAccountName attribute and concatenating it with the KDC realm,
52 * specifically the canonicalized realm of the WELLKNOWN/FEDERATED HDB
55 * The KDC will need to have access to a default credentials cache, or
56 * OpenLDAP will need to be linked against a version of Cyrus SASL and
57 * Heimdal that supports keytab credentials.
63 #include <common_plugin.h>
64 #include <heimqueue.h>
66 #include <gssapi/gssapi.h>
67 #include <gssapi_mech.h>
71 #include "gss_preauth_authorizer_plugin.h"
73 #ifndef PAC_REQUESTOR_SID
74 #define PAC_REQUESTOR_SID 18
77 struct ad_server_tuple
{
78 HEIM_TAILQ_ENTRY(ad_server_tuple
) link
;
81 #ifdef LDAP_OPT_X_SASL_GSS_CREDS
82 gss_cred_id_t gss_cred
;
86 struct altsecid_gss_preauth_authorizer_context
{
87 HEIM_TAILQ_HEAD(ad_server_tuple_list
, ad_server_tuple
) servers
;
91 sasl_interact(LDAP
*ld
, unsigned flags
, void *defaults
, void *interact
)
96 #ifdef LDAP_OPT_X_SASL_GSS_CREDS
97 static krb5_error_code
98 ad_acquire_cred(krb5_context context
,
99 krb5_const_realm realm
,
100 struct ad_server_tuple
*server
)
102 const char *keytab_name
= NULL
;
103 char *keytab_name_buf
= NULL
;
106 OM_uint32 major
, minor
;
107 gss_key_value_element_desc client_keytab
;
108 gss_key_value_set_desc cred_store
;
109 gss_OID_set_desc desired_mechs
;
111 desired_mechs
.count
= 1;
112 desired_mechs
.elements
= GSS_KRB5_MECHANISM
;
114 keytab_name
= krb5_config_get_string(context
, NULL
, "kdc", realm
,
115 "gss_altsecid_authorizer_keytab_name", NULL
);
116 if (keytab_name
== NULL
)
117 keytab_name
= krb5_config_get_string(context
, NULL
, "kdc",
118 "gss_altsecid_authorizer_keytab_name", NULL
);
119 if (keytab_name
== NULL
) {
120 ret
= _krb5_kt_client_default_name(context
, &keytab_name_buf
);
124 keytab_name
= keytab_name_buf
;
127 client_keytab
.key
= "client_keytab";
128 client_keytab
.value
= keytab_name
;
130 cred_store
.count
= 1;
131 cred_store
.elements
= &client_keytab
;
133 major
= gss_acquire_cred_from(&minor
, GSS_C_NO_NAME
, GSS_C_INDEFINITE
,
134 &desired_mechs
, GSS_C_INITIATE
,
135 &cred_store
, &server
->gss_cred
, NULL
, NULL
);
136 if (GSS_ERROR(major
))
137 ret
= minor
? minor
: KRB5_KT_NOTFOUND
;
141 krb5_xfree(keytab_name_buf
);
148 is_recoverable_ldap_err_p(int lret
)
151 (lret
== LDAP_SERVER_DOWN
||
152 lret
== LDAP_TIMEOUT
||
153 lret
== LDAP_UNAVAILABLE
||
155 lret
== LDAP_CONNECT_ERROR
);
158 static krb5_error_code
159 ad_connect(krb5_context context
,
160 krb5_const_realm realm
,
161 struct ad_server_tuple
*server
)
167 } *s
, *servers
= NULL
;
168 size_t i
, num_servers
= 0;
170 if (krb5_config_get_bool(context
, NULL
, "libdefaults", "block_dns",
172 ret
= KRB5KDC_ERR_SVC_UNAVAILABLE
;
173 krb5_set_error_message(context
, ret
, "DNS blocked when finding AD DC");
178 struct rk_dns_reply
*r
;
179 struct rk_resource_record
*rr
;
182 asprintf(&domain
, "_ldap._tcp.%s", realm
);
183 if (domain
== NULL
) {
184 ret
= krb5_enomem(context
);
188 r
= rk_dns_lookup(domain
, "SRV");
191 krb5_set_error_message(context
, KRB5KDC_ERR_SVC_UNAVAILABLE
,
192 "Couldn't find AD DC in DNS");
193 ret
= KRB5KDC_ERR_SVC_UNAVAILABLE
;
197 for (rr
= r
->head
; rr
!= NULL
; rr
= rr
->next
) {
198 if (rr
->type
!= rk_ns_t_srv
)
200 s
= realloc(servers
, sizeof(*servers
) * (num_servers
+ 1));
202 ret
= krb5_enomem(context
);
208 servers
[num_servers
- 1].port
= rr
->u
.srv
->port
;
209 servers
[num_servers
- 1].server
= strdup(rr
->u
.srv
->target
);
214 #ifdef LDAP_OPT_X_SASL_GSS_CREDS
215 if (server
->gss_cred
== GSS_C_NO_CREDENTIAL
) {
216 ret
= ad_acquire_cred(context
, realm
, server
);
222 for (i
= 0; i
< num_servers
; i
++) {
223 int lret
, version
= LDAP_VERSION3
;
227 asprintf(&url
, "ldap://%s:%d", servers
[i
].server
, servers
[i
].port
);
229 ret
= krb5_enomem(context
);
233 lret
= ldap_initialize(&ld
, url
);
235 if (lret
!= LDAP_SUCCESS
)
238 ldap_set_option(ld
, LDAP_OPT_PROTOCOL_VERSION
, &version
);
239 ldap_set_option(ld
, LDAP_OPT_REFERRALS
, LDAP_OPT_OFF
);
240 #ifdef LDAP_OPT_X_SASL_GSS_CREDS
241 ldap_set_option(ld
, LDAP_OPT_X_SASL_GSS_CREDS
, server
->gss_cred
);
244 lret
= ldap_sasl_interactive_bind_s(ld
, NULL
, "GSS-SPNEGO",
245 NULL
, NULL
, LDAP_SASL_QUIET
,
246 sasl_interact
, NULL
);
247 if (lret
!= LDAP_SUCCESS
) {
248 krb5_set_error_message(context
, 0,
249 "Couldn't bind to AD DC %s:%d: %s",
250 servers
[i
].server
, servers
[i
].port
,
251 ldap_err2string(lret
));
252 ldap_unbind_ext_s(ld
, NULL
, NULL
);
260 ret
= (server
->ld
!= NULL
) ? 0 : KRB5_KDC_UNREACH
;
263 for (i
= 0; i
< num_servers
; i
++)
264 free(servers
[i
].server
);
267 if (ret
&& server
->ld
) {
268 ldap_unbind_ext_s(server
->ld
, NULL
, NULL
);
275 static krb5_error_code
276 ad_lookup(krb5_context context
,
277 krb5_const_realm realm
,
278 struct ad_server_tuple
*server
,
279 gss_const_name_t initiator_name
,
280 gss_const_OID mech_type
,
281 krb5_principal
*canon_principal
,
282 kdc_data_t
*requestor_sid
)
286 const char *mech_type_str
, *p
;
288 gss_buffer_desc initiator_name_buf
= GSS_C_EMPTY_BUFFER
;
289 LDAPMessage
*m
= NULL
, *m0
;
292 char *attrs
[] = { "sAMAccountName", "objectSid", NULL
};
293 struct berval
**values
= NULL
;
295 *canon_principal
= NULL
;
297 *requestor_sid
= NULL
;
299 mech_type_str
= gss_oid_to_name(mech_type
);
300 if (mech_type_str
== NULL
) {
301 ret
= KRB5_PREAUTH_BAD_TYPE
; /* should never happen */
305 ret
= KRB5_KDC_ERR_CLIENT_NOT_TRUSTED
;
307 if (GSS_ERROR(gss_display_name(&minor
, initiator_name
,
308 &initiator_name_buf
, NULL
)))
311 if ((p
= strrchr(mech_type_str
, '-')) != NULL
) {
312 asprintf(&filter
, "(&(objectClass=user)"
313 "(|(altSecurityIdentities=%.*s:%.*s)(altSecurityIdentities=%s:%.*s)))",
314 (int)(p
- mech_type_str
), mech_type_str
,
315 (int)initiator_name_buf
.length
, (char *)initiator_name_buf
.value
,
317 (int)initiator_name_buf
.length
,
318 (char *)initiator_name_buf
.value
);
320 asprintf(&filter
, "(&(objectClass=user)(altSecurityIdentities=%s:%.*s))",
322 (int)initiator_name_buf
.length
,
323 (char *)initiator_name_buf
.value
);
328 lret
= ldap_domain2dn(realm
, &basedn
);
329 if (lret
!= LDAP_SUCCESS
)
332 lret
= ldap_search_ext_s(server
->ld
, basedn
, LDAP_SCOPE_SUBTREE
,
334 NULL
, NULL
, NULL
, 1, &m
);
335 if (lret
== LDAP_SIZELIMIT_EXCEEDED
)
336 ret
= KRB5KDC_ERR_PRINCIPAL_NOT_UNIQUE
;
337 else if (is_recoverable_ldap_err_p(lret
))
338 ret
= KRB5KDC_ERR_SVC_UNAVAILABLE
;
339 if (lret
!= LDAP_SUCCESS
)
342 m0
= ldap_first_entry(server
->ld
, m
);
346 values
= ldap_get_values_len(server
->ld
, m0
, "sAMAccountName");
347 if (values
== NULL
||
348 ldap_count_values_len(values
) == 0)
351 ret
= krb5_make_principal(context
, canon_principal
, realm
,
352 values
[0]->bv_val
, NULL
);
357 ldap_value_free_len(values
);
359 values
= ldap_get_values_len(server
->ld
, m0
, "objectSid");
360 if (values
== NULL
||
361 ldap_count_values_len(values
) == 0)
364 *requestor_sid
= kdc_data_create(values
[0]->bv_val
, values
[0]->bv_len
);
365 if (*requestor_sid
== NULL
)
372 ret
= krb5_enomem(context
);
377 krb5_free_principal(context
, *canon_principal
);
378 *canon_principal
= NULL
;
381 kdc_object_release(*requestor_sid
);
382 *requestor_sid
= NULL
;
386 ldap_value_free_len(values
);
388 ldap_memfree(basedn
);
390 gss_release_buffer(&minor
, &initiator_name_buf
);
395 static KRB5_LIB_CALL krb5_error_code
398 gss_const_name_t initiator_name
,
399 gss_const_OID mech_type
,
401 krb5_boolean
*authorized
,
402 krb5_principal
*mapped_name
)
404 struct altsecid_gss_preauth_authorizer_context
*c
= ctx
;
405 struct ad_server_tuple
*server
= NULL
;
407 krb5_context context
= kdc_request_get_context((kdc_request_t
)r
);
408 const hdb_entry
*client
= kdc_request_get_client(r
);
409 krb5_const_principal server_princ
= kdc_request_get_server_princ(r
);
410 krb5_const_realm realm
= krb5_principal_get_realm(context
, client
->principal
);
411 krb5_boolean reconnect_p
= FALSE
;
413 kdc_data_t requestor_sid
= NULL
;
418 if (!krb5_principal_is_federated(context
, client
->principal
) ||
419 (ret_flags
& GSS_C_ANON_FLAG
))
420 return KRB5_PLUGIN_NO_HANDLE
;
422 is_tgs
= krb5_principal_is_krbtgt(context
, server_princ
);
424 HEIM_TAILQ_FOREACH(server
, &c
->servers
, link
) {
425 if (strcmp(realm
, server
->realm
) == 0)
429 if (server
== NULL
) {
430 server
= calloc(1, sizeof(*server
));
432 return krb5_enomem(context
);
434 server
->realm
= strdup(realm
);
435 if (server
->realm
== NULL
) {
437 return krb5_enomem(context
);
440 HEIM_TAILQ_INSERT_HEAD(&c
->servers
, server
, link
);
444 if (server
->ld
== NULL
) {
445 ret
= ad_connect(context
, realm
, server
);
450 ret
= ad_lookup(context
, realm
, server
,
451 initiator_name
, mech_type
,
452 mapped_name
, is_tgs
? &requestor_sid
: NULL
);
453 if (ret
== KRB5KDC_ERR_SVC_UNAVAILABLE
) {
454 ldap_unbind_ext_s(server
->ld
, NULL
, NULL
);
457 /* try to reconnect iff we haven't already tried */
458 reconnect_p
= !reconnect_p
;
461 *authorized
= (ret
== 0);
462 } while (reconnect_p
);
465 kdc_request_set_attribute((kdc_request_t
)r
,
466 HSTR("org.h5l.gss-pa-requestor-sid"), requestor_sid
);
467 kdc_object_release(requestor_sid
);
473 static KRB5_LIB_CALL krb5_error_code
474 finalize_pac(void *ctx
, astgs_request_t r
)
476 kdc_data_t requestor_sid
;
478 requestor_sid
= kdc_request_get_attribute((kdc_request_t
)r
,
479 HSTR("org.h5l.gss-pa-requestor-sid"));
480 if (requestor_sid
== NULL
)
483 kdc_audit_setkv_object((kdc_request_t
)r
, "gss_requestor_sid", requestor_sid
);
485 return kdc_request_add_pac_buffer(r
, PAC_REQUESTOR_SID
,
486 kdc_data_get_data(requestor_sid
));
489 static KRB5_LIB_CALL krb5_error_code
490 init(krb5_context context
, void **contextp
)
492 struct altsecid_gss_preauth_authorizer_context
*c
;
494 c
= calloc(1, sizeof(*c
));
496 return krb5_enomem(context
);
498 HEIM_TAILQ_INIT(&c
->servers
);
504 static KRB5_LIB_CALL
void
507 struct altsecid_gss_preauth_authorizer_context
*c
= context
;
508 struct ad_server_tuple
*server
, *next
;
511 HEIM_TAILQ_FOREACH_SAFE(server
, &c
->servers
, link
, next
) {
513 ldap_unbind_ext_s(server
->ld
, NULL
, NULL
);
514 #ifdef LDAP_OPT_X_SASL_GSS_CREDS
515 gss_release_cred(&minor
, &server
->gss_cred
);
521 static krb5plugin_gss_preauth_authorizer_ftable plug_desc
=
522 { 1, init
, fini
, authorize
, finalize_pac
};
524 static krb5plugin_gss_preauth_authorizer_ftable
*plugs
[] = { &plug_desc
};
527 altsecid_gss_preauth_authorizer_get_instance(const char *libname
)
529 if (strcmp(libname
, "krb5") == 0)
530 return krb5_get_instance(libname
);
531 if (strcmp(libname
, "kdc") == 0)
532 return kdc_get_instance(libname
);
536 krb5_plugin_load_ft kdc_gss_preauth_authorizer_plugin_load
;
538 krb5_error_code KRB5_CALLCONV
539 kdc_gss_preauth_authorizer_plugin_load(heim_pcontext context
,
540 krb5_get_instance_func_t
*get_instance
,
542 krb5_plugin_common_ftable_cp
**plugins
)
544 *get_instance
= altsecid_gss_preauth_authorizer_get_instance
;
545 *num_plugins
= sizeof(plugs
) / sizeof(plugs
[0]);
546 *plugins
= (krb5_plugin_common_ftable_cp
*)plugs
;