xref: /illumos-gate/usr/src/cmd/smbsrv/smbd/smbd_krb5ssp.c (revision dd72704bd9e794056c558153663c739e2012d721)
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
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
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
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
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
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