/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* or http://www.opensolaris.org/os/licensing.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
*/
/*
* SMBFS GSS-API
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <netdb.h>
#include <sys/types.h>
#include <smb/smb.h>
#include <netsmb/mchain.h>
#include <gssapi/gssapi.h>
#include <gssapi/gssapi_ext.h>
#include <smbsrv/libsmbns.h>
#include <smbsrv/libsmb.h>
#include "smbfs_lib.h"
#include "smbfs_private.h"
#define SMBFS_GSS_OID_SPNEGO "1.3.6.1.5.5.2"
typedef struct smbfs_session_key_t {
uint16_t len;
uint8_t *val;
} smbfs_session_key_t;
/*
* smbfs_gss_error
*
* Display GSS error message from error code. This routine is used to display
* the mechanism independent and mechanism specific error messages for GSS
* routines. The major status error code is the mechanism independent error
* code and the minor status error code is the mechanism specific error code.
*/
static void
smbfs_gss_error(char *name, OM_uint32 maj, OM_uint32 min)
{
gss_buffer_desc msg;
OM_uint32 msg_ctx = 0;
OM_uint32 status, min2;
status = gss_display_status(&min2, maj, GSS_C_GSS_CODE, GSS_C_NULL_OID,
&msg_ctx, &msg);
if ((status != GSS_S_COMPLETE) && (status != GSS_S_CONTINUE_NEEDED))
return;
if (msg.value == NULL)
return;
DPRINT("SMBFS (gss-api): %s: GSS major status error: %s",
name, (char *)msg.value);
(void) gss_release_buffer(&min2, &msg);
status = gss_display_status(&min2, min, GSS_C_MECH_CODE,
GSS_C_NULL_OID, &msg_ctx, &msg);
if ((status != GSS_S_COMPLETE) && (status != GSS_S_CONTINUE_NEEDED))
return;
if (msg.value == NULL)
return;
DPRINT("SMBFS (gss-api): %s: GSS minor status error: %s",
name, (char *)msg.value);
(void) gss_release_buffer(&min2, &msg);
}
/*
* smbfs_gss_get_targetname
*
* Returns a GSS-API compatible target name for the host target.
* The format of the target name is
* <SAM account>@<REALM>
* where SAM account is in the format of <NetBIOS name>$
* NetBIOS name is a hostname truncated at first '.' or 15 bytes, whichever
* occurs first, and converted to uppercase.
*
* The function return 0 on success and -1 on failure.
*/
static int
smbfs_gss_get_targetname(const char *hostname, const char *drealm,
gss_name_t *target_name)
{
gss_buffer_desc svcbuf;
OM_uint32 minor;
char buf[MAXHOSTNAMELEN], *p, *name;
int num_bytes, buflen;
if (*drealm == '\0')
return (-1);
(void) strlcpy(buf, hostname, MAXHOSTNAMELEN);
if ((p = strchr(buf, '.')) != NULL)
*p = '\0';
buflen = strlen(buf);
if (buflen >= NETBIOS_NAME_SZ)
buf[NETBIOS_NAME_SZ - 1] = '\0';
(void) smb_strupr(buf);
if ((num_bytes = asprintf(&name, "%s$@%s", buf, drealm)) == -1)
return (-1);
svcbuf.length = num_bytes;
svcbuf.value = name;
if ((gss_import_name(&minor, &svcbuf, GSS_C_NO_OID,
target_name)) != GSS_S_COMPLETE) {
free(svcbuf.value);
return (-1);
}
free(svcbuf.value);
return (0);
}
/*
* smbfs_gss_tok2mbdata
*
* Converts input GSS token to output mbdata buffer format for sending to SMB
* server.
*/
static int
smbfs_gss_tok2mbdata(gss_buffer_desc *in, struct mbdata *out)
{
mbuf_t *m;
int length;
length = in->length;
if ((length == 0) || (smbfs_mb_init_sz(out, length) != 0))
return (-1);
m = out->mb_top;
if (m == NULL)
return (-1);
m->m_data = in->value;
out->mb_count = m->m_len = length;
return (0);
}
/*
* smbfs_gss_mbdata2tok
*
* Converts received mbdata buffer format to GSS token for processing by SMB
* client.
*/
static int
smbfs_gss_mbdata2tok(struct mbdata *in, gss_buffer_desc *out)
{
mbuf_t *m;
m = in->mb_top;
if ((m == NULL) || (m->m_len == 0))
return (-1);
if ((out->value = calloc(1, m->m_len)) == NULL)
return (-1);
(void) memcpy(out->value, m->m_data, m->m_len);
out->length = m->m_len;
return (0);
}
/*
* smbfs_gss_release_oid
*
* Release the gss_OID if it was allocated.
*/
static void
smbfs_gss_release_oid(gss_OID *oid)
{
OM_uint32 min;
if (oid && *oid == GSS_C_NULL_OID)
return;
(void) gss_release_oid(&min, oid);
}
/*
* smbfs_gss_cleanup
*
* Cleans up the GSS-API objects allocated.
*/
static void
smbfs_gss_cleanup(OM_uint32 *min_stat, gss_ctx_id_t *context,
gss_cred_id_t *cred, gss_name_t *trgt_name, gss_OID *mech_oid,
gss_buffer_desc *input_tok, gss_buffer_desc *send_tok)
{
if (*context != GSS_C_NO_CONTEXT)
(void) gss_delete_sec_context(min_stat,
context, GSS_C_NO_BUFFER);
if (*cred != GSS_C_NO_CREDENTIAL)
(void) gss_release_cred(min_stat, cred);
(void) gss_release_name(min_stat, trgt_name);
smbfs_gss_release_oid(mech_oid);
if (input_tok && input_tok->length > 0)
(void) gss_release_buffer(min_stat, input_tok);
if (send_tok && send_tok->length > 0)
(void) gss_release_buffer(min_stat, send_tok);
}
/*
* smbfs_gss_inquire_cred
*
* This function is used to inquire the validity of passed credentials.
* Sets errno to ETIME, if the lifetime of the credentials expired.
*
* To account for clock sync. issues between Solaris Client and KDC,
* we will return ETIME within 5 minutes of the reported lifetime. This will
* result in re-acquiring of Kerberos credentials.
*/
static int
smbfs_gss_inquire_cred(gss_cred_id_t *cred)
{
gss_name_t name;
OM_uint32 lifetime;
gss_buffer_desc tname;
OM_uint32 maj_stat, min_stat;
maj_stat = gss_inquire_cred(&min_stat, *cred, &name, &lifetime,
NULL, NULL);
if (maj_stat != GSS_S_COMPLETE) {
smbfs_gss_error("inquiring credentials", maj_stat, min_stat);
return (-1);
}
maj_stat = gss_display_name(&min_stat, name, &tname, NULL);
if (maj_stat != GSS_S_COMPLETE) {
(void) gss_release_name(&min_stat, &name);
return (-1);
}
(void) gss_release_name(&min_stat, &name);
DPRINT("Cred=\"%.*s\", lifetime %d\n",
(int)tname.length, (char *)tname.value, lifetime);
(void) gss_release_buffer(&min_stat, &tname);
if (lifetime <= 300) {
errno = ETIME;
return (-1);
}
return (0);
}
/*
* smbfs_gss_cred_import_name
*
* This function converts the specified credential principal into
* internal gss_import_name format.
*
* Returns 0 on success, -1 on failure.
*/
static int
smbfs_gss_cred_import_name(struct smb_ctx *ctx, gss_name_t *user_name)
{
gss_buffer_desc name;
OM_uint32 maj_stat, min_stat;
gss_OID oid;
char *princ;
int rc = 0;
char sam_acct[SMB_SAMACCT_MAXLEN];
if (smb_is_samaccount(ctx->ct_user)) {
bzero(&sam_acct, sizeof (sam_acct));
if (smb_getsamaccount(sam_acct, sizeof (sam_acct)) != 0)
return (-1);
if (asprintf(&princ, "%s@%s", sam_acct, ctx->ct_realm) == -1)
return (-1);
} else {
if (asprintf(&princ, "%s@%s", ctx->ct_user,
ctx->ct_realm) == -1)
return (-1);
}
oid = GSS_C_NT_USER_NAME;
name.value = princ;
DPRINT("credential for \"%.*s\"", (int)name.length, (char *)name.value);
name.length = strlen(name.value) + 1;
maj_stat = gss_import_name(&min_stat, &name, oid, user_name);
if (maj_stat != GSS_S_COMPLETE) {
smbfs_gss_error("importing name", maj_stat, min_stat);
rc = -1;
}
free(name.value);
return (rc);
}
/*
* smbfs_gss_acquire_cred
*
* SMBFS client needs to acquire the credentials of the user in order to
* initiate security context establishment. Acquiring credentials of a user
* will be performed by using the gss_acquire_cred() function of GSS-API.
*
* For outbound DC request, the credential cache of SMB server
* will be used to query for client credentials.
*
* For all other type of requests, the credential cache will be queried in the
* following order,
* - Default Kerberos client credential cache.
* - SMB client credential cache.
*
* Return 0 on success, -1 on failure.
* Sets errno to ETIME, if the lifetime of the credentials expired.
*/
static int
smbfs_gss_acquire_cred(struct smb_ctx *ctx, gss_cred_id_t *cred)
{
gss_name_t user_name;
OM_uint32 maj_stat, min_stat;
int i, rc = 0;
if (smbfs_gss_cred_import_name(ctx, &user_name) != 0)
return (-1);
if (ctx->ct_rflags & SMBFS_OUTBOUNDDC) {
for (i = SMBFS_CCACHE_SRV_SYS;
i < SMBFS_CCACHE_SRV_MAX; i++) {
if ((rc = smbfs_ccache_srv_set(ctx, i)) != 0)
break;
maj_stat = gss_acquire_cred(&min_stat, user_name, 0,
GSS_C_NULL_OID_SET, GSS_C_INITIATE, cred,
NULL, NULL);
if (maj_stat == GSS_S_COMPLETE)
break;
}
} else {
for (i = SMBFS_CCACHE_CLNT_DEFAULT;
i < SMBFS_CCACHE_CLNT_MAX; i++) {
if ((rc = smbfs_ccache_clnt_set(ctx, i)) != 0)
break;
maj_stat = gss_acquire_cred(&min_stat, user_name, 0,
GSS_C_NULL_OID_SET, GSS_C_INITIATE, cred,
NULL, NULL);
if (maj_stat == GSS_S_COMPLETE)
break;
}
}
if ((rc != 0) || (maj_stat != GSS_S_COMPLETE)) {
smbfs_gss_error("acquiring credentials", maj_stat,
min_stat);
(void) gss_release_name(&min_stat, &user_name);
return (-1);
}
errno = 0;
if (smbfs_gss_inquire_cred(cred) != 0) {
(void) gss_release_name(&min_stat, &user_name);
return (-1);
}
(void) gss_release_name(&min_stat, &user_name);
return (0);
}
/*
* smbfs_gss_is_cred
*
* This function returns B_TRUE if credentials can be obtained for the client
* context. If the existing credentials have expired, attempt will be made to
* renew the expired credentials.
*
* Returns B_FALSE on error.
*/
boolean_t
smbfs_gss_is_cred(struct smb_ctx *ctx)
{
gss_cred_id_t cred = GSS_C_NO_CREDENTIAL;
OM_uint32 min_stat;
boolean_t ret = B_FALSE;
if (smbfs_gss_acquire_cred(ctx, &cred) != 0) {
if ((errno == ETIME) && (smbfs_kinit(ctx, B_TRUE) == 0))
ret = B_TRUE;
} else {
ret = B_TRUE;
}
if (cred != GSS_C_NO_CREDENTIAL)
(void) gss_release_cred(&min_stat, &cred);
return (ret);
}
/*
* smbfs_session_key_create
*
* Allocate and create a session key.
*/
static int
smbfs_session_key_create(smbfs_session_key_t *ssnkey, uint8_t *buf,
uint32_t len)
{
assert(ssnkey != NULL);
assert(buf != NULL);
if ((ssnkey->val = calloc(1, len)) == NULL)
return (-1);
ssnkey->len = len;
bcopy(buf, ssnkey->val, ssnkey->len);
return (0);
}
/*
* smbfs_session_key_destroy
*
* Destroys a session key.
*/
static void
smbfs_session_key_destroy(smbfs_session_key_t *ssnkey)
{
if (smb_session_key_valid((smb_session_key_t *)ssnkey)) {
free(ssnkey->val);
bzero(ssnkey, sizeof (smbfs_session_key_t));
}
}
/*
* smbfs_extract_ssnkey
*
* Extracts session key from KRB_AP_REP message using
* gss_inquire_sec_context_by_oid API.
*
* The data set returned by the GSS inquiry function is composed
* of two elements:
* 1st element: the actual session key value and length
* 2nd element: the associated key encryption type by OID.
*/
static int
smbfs_extract_ssnkey(gss_ctx_id_t context, smbfs_session_key_t *session_key)
{
OM_uint32 maj_stat, min_stat;
gss_buffer_set_t data_set = GSS_C_NO_BUFFER_SET;
int rc;
if (session_key == NULL)
return (-1);
bzero(session_key, sizeof (smbfs_session_key_t));
maj_stat = gss_inquire_sec_context_by_oid(&min_stat,
context, GSS_C_INQ_SSPI_SESSION_KEY, &data_set);
if (GSS_ERROR(maj_stat)) {
smbfs_gss_error("signing session key", maj_stat, min_stat);
return (-1);
}
if (data_set == GSS_C_NO_BUFFER_SET ||
data_set->count == 0 ||
data_set->elements[0].length == 0 ||
data_set->elements[0].value == NULL) {
DPRINT("no session key");
return (-1);
}
if ((rc = smbfs_session_key_create(session_key,
data_set->elements[0].value, data_set->elements[0].length)) != 0)
DPRINT("unable to create smbfs session key");
(void) gss_release_buffer_set(&min_stat, &data_set);
return (rc);
}
/*
* smbfs_gss_setkey_signing
*
* Called after successful authentication.
* Setup the session key and MAC key for signing.
*
* [MS-KILE] section 3.1.1.2
*
* The subkey in the EncAPRepPart of the KRB_AP_REP message SHOULD be used as
* the session key when MutualAuthentication is requested. (The KRB_AP_REP
* message and its fields are defined in section 5.5.2 of [RFC4120].) When DES
* and RC4 are used, the implementation is as described in [RFC1964]. With DES
* and RC4, the subkey in the KRB_AP_REQ message can be used as the session key,
* as it is the same as the subkey in KRB_AP_REP message; however when AES is
* used (see [RFC4121]), the subkeys are different and the subkey in the
* KRB_AP_REP SHOULD be used. (The KRB_AP_REQ message is defined in section
* 5.5.1 of [RFC4120]).
*/
static int
smbfs_gss_setkey_signing(gss_ctx_id_t context, struct smb_ctx *ctx)
{
int len;
smbfs_session_key_t ssn_key;
if (smbfs_extract_ssnkey(context, &ssn_key) != 0)
return (-1);
(void) memset(ctx->ct_ssn_key, 0, SMBIOC_HASH_SZ);
if ((len = ssn_key.len) > SMBIOC_HASH_SZ)
len = SMBIOC_HASH_SZ;
(void) memcpy(ctx->ct_ssn_key, ssn_key.val, len);
if ((ctx->ct_hflags2 & SMB_FLAGS2_SMB_SECURITY_SIGNATURE) &&
(ctx->ct_mackey == NULL)) {
ctx->ct_mackeylen = ssn_key.len;
if ((ctx->ct_mackey = calloc(1, ctx->ct_mackeylen)) == NULL) {
ctx->ct_mackeylen = 0;
smbfs_session_key_destroy(&ssn_key);
return (-1);
}
(void) memcpy(ctx->ct_mackey, ssn_key.val,
ctx->ct_mackeylen);
ctx->ct_mac_seqno = 2;
}
return (0);
}
/*
* smbfs_gss_setflags_signing
*
* Setup flags to do signing.
*/
static void
smbfs_gss_setflags_signing(struct smb_ctx *ctx)
{
if (ctx->ct_vcflags & SMBV_WILL_SIGN) {
if (ctx->ct_vopt & SMBVOPT_SIGNING_REQUIRED) {
ctx->ct_hflags2 |=
SMB_FLAGS2_SMB_SECURITY_SIGNATURE_REQUIRED;
}
ctx->ct_hflags2 |= SMB_FLAGS2_SMB_SECURITY_SIGNATURE;
}
}
/*
* smbfs_gss_init_sec
*
* Perform GSS context-establishement with the SMB server.
*
* [MS-SMB] If the capabilities returned in the SMB_COM_NEGOTIATE response
* include CAP_EXTENDED_SECURITY, the client MUST set the GSSNegotiateToken to
* the value returned in the SecurityBlob field in the SMB_COM_NEGOTIATE server
* response. The client MUST initiate the GSS authentication protocol via
* the gss_init_sec_context() interface, as specified in [RFC2743], by passing
* in the input GSSNegotiateToken.
*
* In our case, input_tok contains the value of the Security blob field
* returned by the SMB server in SMB_COM_NEGOTIATE response. This input_tok
* is assigned to token_ptr on the first pass, which is then passed to
* gss_init_sec_context() function.
*
* The input GSS mechanism OID (mech_OID) is set to the OID of the SPNEGO
* mechanism in Solaris.
*
* Every token generated by gss_init_sec_context() is stored in send_tok
* which is then transmitted to the server; every received token is stored in
* recv_tok, which token_ptr is then set to, to be processed by the next call
* to gss_init_sec_context.
*
* GSS-API guarantees that send_tok's length will be non-zero
* if and only if the server is expecting another token from us,
* and that gss_init_sec_context returns GSS_S_CONTINUE_NEEDED if
* and only if the server has another token to send us.
*
* Returns 0 on success, EAUTH on failure.
* If the first request has already successfully send, then failure to process
* subsequest requests will return EBADE.
*/
int
smbfs_gss_init_sec(struct smb_ctx *ctx, struct mbdata *caller_in)
{
gss_buffer_desc send_tok, recv_tok, input_tok, gss_str, *token_ptr;
gss_name_t trgt_name;
gss_OID mech_oid, mech_type;
gss_ctx_id_t gss_context;
gss_cred_id_t gss_cred = GSS_C_NO_CREDENTIAL;
OM_uint32 maj_stat, min_stat, init_sec_min_stat, ret_flags;
uint32_t gss_flags, ntstatus;
uint16_t action = 0;
int recv_len = 0;
int rc = 0;
struct mbdata send_mb, recv_mb;
char *str;
boolean_t is_ssn_setup = B_FALSE;
bzero(&send_mb, sizeof (struct mbdata));
bzero(&recv_mb, sizeof (struct mbdata));
if ((ctx->ct_authflags & SMB_AT_KRB5) == 0)
return (EAUTH);
if (smbfs_gss_get_targetname(ctx->ct_srvname, ctx->ct_realm,
&trgt_name) != 0)
return (EAUTH);
if (smbfs_gss_acquire_cred(ctx, &gss_cred) != 0) {
(void) gss_release_name(&min_stat, &trgt_name);
return (EAUTH);
}
str = SMBFS_GSS_OID_SPNEGO;
gss_str.value = str;
gss_str.length = strlen(str);
maj_stat = gss_str_to_oid(&min_stat, &gss_str, &mech_oid);
if (maj_stat != GSS_S_COMPLETE) {
(void) gss_release_cred(&min_stat, &gss_cred);
(void) gss_release_name(&min_stat, &trgt_name);
return (EAUTH);
}
if (smbfs_gss_mbdata2tok(caller_in, &input_tok) != 0) {
smbfs_gss_release_oid(&mech_oid);
(void) gss_release_cred(&min_stat, &gss_cred);
(void) gss_release_name(&min_stat, &trgt_name);
return (EAUTH);
}
token_ptr = &input_tok;
gss_context = GSS_C_NO_CONTEXT;
gss_flags = GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG;
do {
maj_stat = gss_init_sec_context(
&init_sec_min_stat,
gss_cred,
&gss_context,
trgt_name,
mech_oid,
gss_flags,
0,
NULL,
token_ptr,
&mech_type,
&send_tok,
&ret_flags,
NULL);
if (recv_len > 0)
(void) gss_release_buffer(&min_stat, &recv_tok);
if (maj_stat != GSS_S_COMPLETE &&
maj_stat != GSS_S_CONTINUE_NEEDED) {
smbfs_gss_error("initializing context", maj_stat,
init_sec_min_stat);
smbfs_gss_cleanup(&min_stat, &gss_context, &gss_cred,
&trgt_name, &mech_oid, &input_tok, &send_tok);
return (is_ssn_setup ? EBADE : EAUTH);
}
if (send_tok.length > 0) {
if (smbfs_gss_tok2mbdata(&send_tok, &send_mb) != 0) {
smbfs_gss_cleanup(&min_stat, &gss_context,
&gss_cred, &trgt_name, &mech_oid,
&input_tok, &send_tok);
return (is_ssn_setup ? EBADE : EAUTH);
}
smbfs_gss_setflags_signing(ctx);
if (smbfs_ssnsetup(ctx, &send_mb, &recv_mb,
&ntstatus, &action) < 0) {
smbfs_gss_cleanup(&min_stat, &gss_context,
&gss_cred, &trgt_name, &mech_oid,
&input_tok, &send_tok);
return (is_ssn_setup ? EBADE : EAUTH);
}
is_ssn_setup = B_TRUE;
}
(void) gss_release_buffer(&min_stat, &send_tok);
(void) gss_release_buffer(&min_stat, &input_tok);
if (maj_stat == GSS_S_CONTINUE_NEEDED) {
if (smbfs_gss_mbdata2tok(&recv_mb, &recv_tok) != 0) {
smbfs_gss_cleanup(&min_stat, &gss_context,
&gss_cred, &trgt_name, &mech_oid,
NULL, NULL);
return (is_ssn_setup ? EBADE : EAUTH);
}
token_ptr = &recv_tok;
recv_len = token_ptr->length;
}
} while (maj_stat == GSS_S_CONTINUE_NEEDED);
if (smbfs_gss_setkey_signing(gss_context, ctx) != 0)
rc = is_ssn_setup ? EBADE : EAUTH;
smbfs_gss_cleanup(&min_stat, &gss_context, &gss_cred, &trgt_name,
&mech_oid, NULL, NULL);
return (rc);
}