1 /*
2 * This file and its contents are supplied under the terms of the
3 * Common Development and Distribution License ("CDDL"), version 1.0.
4 * You may only use this file in accordance with the terms of version
5 * 1.0 of the CDDL.
6 *
7 * A full copy of the text of the CDDL should have accompanied this
8 * source. A copy of the CDDL is also available via the Internet at
9 * http://www.illumos.org/license/CDDL.
10 */
11
12 /*
13 * Copyright 2022 Tintri by DDN, Inc. All rights reserved.
14 */
15
16 /*
17 * SPNEGO back-end for Kerberos. See [MS-KILE]
18 */
19
20 #include <sys/types.h>
21 #include <gssapi/gssapi_ext.h>
22 #include <gssapi/gssapi_krb5.h>
23 #include <krb5.h>
24 #include "smbd.h"
25 #include "smbd_authsvc.h"
26
27 /* From krb5/krb/pac.c (should have been exported) */
28 #define PAC_LOGON_INFO 1
29
30 typedef struct krb5ssp_backend {
31 gss_ctx_id_t be_gssctx;
32 char *be_username;
33 gss_buffer_desc be_authz_pac;
34 krb5_context be_kctx;
35 krb5_pac be_kpac;
36 krb5_data be_pac;
37 } krb5ssp_backend_t;
38
39 static uint32_t
40 get_authz_data_pac(
41 gss_ctx_id_t context_handle,
42 gss_buffer_t ad_data);
43
44 static uint32_t
45 get_ssnkey(authsvc_context_t *ctx);
46
47
48 /*
49 * Initialize this context for Kerberos, if possible.
50 *
51 * Should not get here unless libsmb smb_config_get_negtok
52 * includes the Kerberos5 Mech OIDs in our spnego hint.
53 *
54 * Todo: allocate ctx->ctx_backend
55 * See: krb5_gss_accept_sec_context()
56 */
57 int
smbd_krb5ssp_init(authsvc_context_t * ctx)58 smbd_krb5ssp_init(authsvc_context_t *ctx)
59 {
60 krb5ssp_backend_t *be;
61
62 be = malloc(sizeof (*be));
63 if (be == 0)
64 return (NT_STATUS_NO_MEMORY);
65 bzero(be, sizeof (*be));
66 be->be_gssctx = GSS_C_NO_CONTEXT;
67 ctx->ctx_backend = be;
68
69 return (0);
70 }
71
72 /*
73 * Todo: free ctx->ctx_backend
74 */
75 void
smbd_krb5ssp_fini(authsvc_context_t * ctx)76 smbd_krb5ssp_fini(authsvc_context_t *ctx)
77 {
78 krb5ssp_backend_t *be = ctx->ctx_backend;
79 uint32_t minor;
80
81 if (be == NULL)
82 return;
83
84 if (be->be_kctx != NULL) {
85 krb5_free_data_contents(be->be_kctx, &be->be_pac);
86
87 if (be->be_kpac != NULL)
88 krb5_pac_free(be->be_kctx, be->be_kpac);
89
90 krb5_free_context(be->be_kctx);
91 }
92
93 (void) gss_release_buffer(NULL, &be->be_authz_pac);
94
95 free(be->be_username);
96
97 if (be->be_gssctx != GSS_C_NO_CONTEXT) {
98 (void) gss_delete_sec_context(&minor, &be->be_gssctx,
99 GSS_C_NO_BUFFER);
100 }
101
102 free(be);
103 }
104
105 static char *krb5ssp_def_username = "Unknown-Kerberos-User";
106 static char *krb5ssp_def_domain = "Unknown-Domain";
107
108 /*
109 * Handle a Kerberos auth message.
110 *
111 * State across messages is in ctx->ctx_backend.
112 *
113 * Equivalent to smbd_user_auth_logon().
114 */
115 int
smbd_krb5ssp_work(authsvc_context_t * ctx)116 smbd_krb5ssp_work(authsvc_context_t *ctx)
117 {
118 gss_buffer_desc intok, outtok;
119 gss_buffer_desc namebuf;
120 krb5ssp_backend_t *be = ctx->ctx_backend;
121 gss_name_t gname = NULL;
122 OM_uint32 major, minor, ret_flags;
123 gss_OID name_type = GSS_C_NULL_OID;
124 gss_OID mech_type = GSS_C_NULL_OID;
125 krb5_error_code kerr;
126 uint32_t status;
127 smb_token_t *token = NULL;
128
129 intok.length = ctx->ctx_ibodylen;
130 intok.value = ctx->ctx_ibodybuf;
131 bzero(&outtok, sizeof (gss_buffer_desc));
132 bzero(&namebuf, sizeof (gss_buffer_desc));
133
134 assert(be->be_username == NULL);
135
136 /* Do this early, for error message support. */
137 kerr = krb5_init_context(&be->be_kctx);
138 if (kerr != 0) {
139 smbd_report("krb5ssp, krb5_init_ctx: %s",
140 krb5_get_error_message(be->be_kctx, kerr));
141 return (NT_STATUS_INTERNAL_ERROR);
142 }
143
144 major = gss_accept_sec_context(&minor, &be->be_gssctx,
145 GSS_C_NO_CREDENTIAL, &intok,
146 GSS_C_NO_CHANNEL_BINDINGS, &gname, &mech_type, &outtok,
147 &ret_flags, NULL, NULL);
148
149 if (outtok.length == 0)
150 ctx->ctx_obodylen = 0;
151 else if (outtok.length <= ctx->ctx_obodylen) {
152 ctx->ctx_obodylen = outtok.length;
153 (void) memcpy(ctx->ctx_obodybuf, outtok.value, outtok.length);
154 free(outtok.value);
155 outtok.value = NULL;
156 } else {
157 free(ctx->ctx_obodybuf);
158 ctx->ctx_obodybuf = outtok.value;
159 ctx->ctx_obodylen = outtok.length;
160 outtok.value = NULL;
161 }
162
163 if (GSS_ERROR(major)) {
164 smbd_report("krb5ssp: gss_accept_sec_context, "
165 "mech=0x%x, major=0x%x, minor=0x%x",
166 (int)mech_type, major, minor);
167 smbd_report(" krb5: %s",
168 krb5_get_error_message(be->be_kctx, minor));
169 status = NT_STATUS_WRONG_PASSWORD;
170 goto out;
171 }
172
173 switch (major) {
174 case GSS_S_COMPLETE:
175 break;
176 case GSS_S_CONTINUE_NEEDED:
177 if (outtok.length > 0) {
178 ctx->ctx_orawtype = LSA_MTYPE_ES_CONT;
179 /* becomes NT_STATUS_MORE_PROCESSING_REQUIRED */
180 return (0);
181 }
182 status = NT_STATUS_WRONG_PASSWORD;
183 goto out;
184 default:
185 status = NT_STATUS_WRONG_PASSWORD;
186 goto out;
187 }
188
189 /*
190 * OK, we got GSS_S_COMPLETE. Get the name so we can use it
191 * in log messages if we get failures decoding the PAC etc.
192 * Then get the PAC, decode it, build the logon token.
193 */
194
195 if (gname != NULL && GSS_S_COMPLETE ==
196 gss_display_name(&minor, gname, &namebuf, &name_type)) {
197 /* Save the user name. */
198 be->be_username = strdup(namebuf.value);
199 (void) gss_release_buffer(&minor, &namebuf);
200 (void) gss_release_name(&minor, &gname);
201 if (be->be_username == NULL) {
202 return (NT_STATUS_NO_MEMORY);
203 }
204 }
205
206 /*
207 * Extract the KRB5_AUTHDATA_WIN2K_PAC data.
208 */
209 status = get_authz_data_pac(be->be_gssctx,
210 &be->be_authz_pac);
211 if (status)
212 goto out;
213
214 kerr = krb5_pac_parse(be->be_kctx, be->be_authz_pac.value,
215 be->be_authz_pac.length, &be->be_kpac);
216 if (kerr) {
217 smbd_report("krb5ssp, krb5_pac_parse: %s",
218 krb5_get_error_message(be->be_kctx, kerr));
219 status = NT_STATUS_UNSUCCESSFUL;
220 goto out;
221 }
222
223 kerr = krb5_pac_get_buffer(be->be_kctx, be->be_kpac,
224 PAC_LOGON_INFO, &be->be_pac);
225 if (kerr) {
226 smbd_report("krb5ssp, krb5_pac_get_buffer: %s",
227 krb5_get_error_message(be->be_kctx, kerr));
228 status = NT_STATUS_UNSUCCESSFUL;
229 goto out;
230 }
231
232 ctx->ctx_token = calloc(1, sizeof (smb_token_t));
233 if (ctx->ctx_token == NULL) {
234 status = NT_STATUS_NO_MEMORY;
235 goto out;
236 }
237
238 status = smb_decode_krb5_pac(ctx->ctx_token, be->be_pac.data,
239 be->be_pac.length);
240 if (status)
241 goto out;
242
243 status = get_ssnkey(ctx);
244 if (status)
245 goto out;
246
247 if (!smb_token_setup_common(ctx->ctx_token)) {
248 status = NT_STATUS_UNSUCCESSFUL;
249 goto out;
250 }
251
252 /* Success! */
253 ctx->ctx_orawtype = LSA_MTYPE_ES_DONE;
254
255 status = 0;
256 token = ctx->ctx_token;
257
258 /*
259 * Before we return, audit successful and failed logons.
260 *
261 * Successful logons are audited using the username and domain
262 * contained in the ticket (where the domain comes from the PAC data).
263 *
264 * Failed logins use a 'default' domain. If we fail after obtaining
265 * the username in the ticket, we audit under that username.
266 *
267 * Prior to decoding the username, we only audit failures where we'll
268 * return NT_STATUS_WRONG_PASSWORD, so that we audit attempts with
269 * invalid (or forged) tickets. These records use a 'default' username;
270 * As such, they serve only to inform an administrator that
271 * a particular client used a bad ticket, but does not contain any
272 * information on the ticket itself.
273 *
274 * Once we have a username, we'll audit all failed authentications.
275 */
276 out:
277 status = smbd_logon_final(token, &ctx->ctx_clinfo.lci_clnt_ipaddr,
278 (be->be_username != NULL) ? be->be_username : krb5ssp_def_username,
279 krb5ssp_def_domain, status);
280
281 return (status);
282 }
283
284 /*
285 * See: GSS_KRB5_EXTRACT_AUTHZ_DATA_FROM_SEC_CONTEXT_OID
286 * and: KRB5_AUTHDATA_WIN2K_PAC
287 */
288 static const gss_OID_desc
289 oid_ex_authz_data_pac = {
290 13, "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02\x05\x0a\x81\x00" };
291
292 /*
293 * See: krb5_gss_inquire_sec_context_by_oid()
294 * and krb5_gss_inquire_sec_context_by_oid_ops[],
295 * gss_krb5int_extract_authz_data_from_sec_context()
296 */
297 static uint32_t
get_authz_data_pac(gss_ctx_id_t context_handle,gss_buffer_t ad_data)298 get_authz_data_pac(
299 gss_ctx_id_t context_handle,
300 gss_buffer_t ad_data)
301 {
302 gss_buffer_set_t data_set = GSS_C_NO_BUFFER_SET;
303 OM_uint32 major, minor;
304 uint32_t status = NT_STATUS_UNSUCCESSFUL;
305
306 if (ad_data == NULL)
307 goto out;
308
309 major = gss_inquire_sec_context_by_oid(
310 &minor,
311 context_handle,
312 (gss_OID)&oid_ex_authz_data_pac,
313 &data_set);
314 if (GSS_ERROR(major)) {
315 smbd_report("krb5ssp, gss_inquire...PAC, "
316 "major=0x%x, minor=0x%x", major, minor);
317 goto out;
318 }
319
320 if ((data_set == GSS_C_NO_BUFFER_SET) || (data_set->count == 0)) {
321 goto out;
322 }
323
324 /* Only need the first element? */
325 ad_data->length = data_set->elements[0].length;
326 ad_data->value = malloc(ad_data->length);
327 if (ad_data->value == NULL) {
328 status = NT_STATUS_NO_MEMORY;
329 goto out;
330 }
331 bcopy(data_set->elements[0].value, ad_data->value, ad_data->length);
332 status = 0;
333
334 out:
335 (void) gss_release_buffer_set(&minor, &data_set);
336
337 return (status);
338 }
339
340 /*
341 * Get the session key, and save it in the token.
342 *
343 * See: krb5_gss_inquire_sec_context_by_oid(),
344 * krb5_gss_inquire_sec_context_by_oid_ops[], and
345 * gss_krb5int_inq_session_key
346 */
347 static uint32_t
get_ssnkey(authsvc_context_t * ctx)348 get_ssnkey(authsvc_context_t *ctx)
349 {
350 krb5ssp_backend_t *be = ctx->ctx_backend;
351 gss_buffer_set_t data_set = GSS_C_NO_BUFFER_SET;
352 OM_uint32 major, minor;
353 size_t keylen;
354 uint32_t status = NT_STATUS_UNSUCCESSFUL;
355
356 major = gss_inquire_sec_context_by_oid(&minor,
357 be->be_gssctx, GSS_C_INQ_SSPI_SESSION_KEY, &data_set);
358 if (GSS_ERROR(major)) {
359 smbd_report("krb5ssp, failed to get session key, "
360 "major=0x%x, minor=0x%x", major, minor);
361 goto out;
362 }
363
364 /*
365 * The key is in the first element
366 */
367 if (data_set == GSS_C_NO_BUFFER_SET ||
368 data_set->count == 0 ||
369 data_set->elements[0].length == 0 ||
370 data_set->elements[0].value == NULL) {
371 smbd_report("krb5ssp: Session key is missing");
372 goto out;
373 }
374 if ((keylen = data_set->elements[0].length) < SMBAUTH_HASH_SZ) {
375 smbd_report("krb5ssp: Session key too short (%d)",
376 data_set->elements[0].length);
377 goto out;
378 }
379
380 ctx->ctx_token->tkn_ssnkey.val = malloc(keylen);
381 if (ctx->ctx_token->tkn_ssnkey.val == NULL) {
382 status = NT_STATUS_NO_MEMORY;
383 goto out;
384 }
385 ctx->ctx_token->tkn_ssnkey.len = keylen;
386 bcopy(data_set->elements[0].value,
387 ctx->ctx_token->tkn_ssnkey.val, keylen);
388 status = 0;
389
390 out:
391 (void) gss_release_buffer_set(&minor, &data_set);
392 return (status);
393 }
394