/*
* 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) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
* Copyright 2014 Nexenta Systems, Inc. All rights reserved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <syslog.h>
#include <netdb.h>
#include <sys/param.h>
#include <kerberosv5/krb5.h>
#include <kerberosv5/com_err.h>
#include <smbsrv/libsmb.h>
#include <smbns_krb.h>
/*
* Kerberized services available on the system.
*/
static smb_krb5_pn_t smb_krb5_pn_tab[] = {
/*
* Service keys are salted with the SMB_KRB_PN_ID_ID_SALT prinipal
* name.
*/
{SMB_KRB5_PN_ID_SALT, SMB_PN_SVC_HOST, SMB_PN_SALT},
/* CIFS SPNs. (HOST, CIFS, ...) */
{SMB_KRB5_PN_ID_HOST_FQHN, SMB_PN_SVC_HOST,
SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR | SMB_PN_UPN_ATTR},
{SMB_KRB5_PN_ID_HOST_SHORT, SMB_PN_SVC_HOST,
SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},
{SMB_KRB5_PN_ID_CIFS_FQHN, SMB_PN_SVC_CIFS,
SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},
{SMB_KRB5_PN_ID_CIFS_SHORT, SMB_PN_SVC_CIFS,
SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},
{SMB_KRB5_PN_ID_MACHINE, NULL,
SMB_PN_KEYTAB_ENTRY},
/* NFS */
{SMB_KRB5_PN_ID_NFS_FQHN, SMB_PN_SVC_NFS,
SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},
/* HTTP */
{SMB_KRB5_PN_ID_HTTP_FQHN, SMB_PN_SVC_HTTP,
SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},
/* ROOT */
{SMB_KRB5_PN_ID_ROOT_FQHN, SMB_PN_SVC_ROOT,
SMB_PN_KEYTAB_ENTRY | SMB_PN_SPN_ATTR},
};
#define SMB_KRB5_SPN_TAB_SZ \
(sizeof (smb_krb5_pn_tab) / sizeof (smb_krb5_pn_tab[0]))
#define SMB_KRB5_MAX_BUFLEN 128
static int smb_krb5_kt_open(krb5_context, char *, krb5_keytab *);
static int smb_krb5_kt_addkey(krb5_context, krb5_keytab, const krb5_principal,
krb5_enctype, krb5_kvno, const krb5_data *, const char *);
static int smb_krb5_spn_count(uint32_t);
static smb_krb5_pn_t *smb_krb5_lookup_pn(smb_krb5_pn_id_t);
static char *smb_krb5_get_pn_by_id(smb_krb5_pn_id_t, uint32_t,
const char *);
static int smb_krb5_get_kprinc(krb5_context, smb_krb5_pn_id_t, uint32_t,
const char *, krb5_principal *);
/*
* Generates a null-terminated array of principal names that
* represents the list of the available Kerberized services
* of the specified type (SPN attribute, UPN attribute, or
* keytab entry).
*
* Returns the number of principal names returned via the 1st
* output parameter (i.e. vals).
*
* Caller must invoke smb_krb5_free_spns to free the allocated
* memory when finished.
*/
uint32_t
smb_krb5_get_pn_set(smb_krb5_pn_set_t *set, uint32_t type, char *fqdn)
{
int cnt, i;
smb_krb5_pn_t *tabent;
if (!set || !fqdn)
return (0);
bzero(set, sizeof (smb_krb5_pn_set_t));
cnt = smb_krb5_spn_count(type);
set->s_pns = (char **)calloc(cnt + 1, sizeof (char *));
if (set->s_pns == NULL)
return (0);
for (i = 0, set->s_cnt = 0; i < SMB_KRB5_SPN_TAB_SZ; i++) {
tabent = &smb_krb5_pn_tab[i];
if (set->s_cnt == cnt)
break;
if ((tabent->p_flags & type) != type)
continue;
set->s_pns[set->s_cnt] = smb_krb5_get_pn_by_id(tabent->p_id,
type, fqdn);
if (set->s_pns[set->s_cnt] == NULL) {
syslog(LOG_ERR, "smbns_ksetpwd: failed to obtain "
"principal names: possible transient memory "
"shortage");
smb_krb5_free_pn_set(set);
return (0);
}
set->s_cnt++;
}
if (set->s_cnt == 0)
smb_krb5_free_pn_set(set);
return (set->s_cnt);
}
void
smb_krb5_free_pn_set(smb_krb5_pn_set_t *set)
{
int i;
if (set == NULL || set->s_pns == NULL)
return;
for (i = 0; i < set->s_cnt; i++)
free(set->s_pns[i]);
free(set->s_pns);
set->s_pns = NULL;
}
/*
* Initialize the kerberos context.
* Return 0 on success. Otherwise, return -1.
*/
int
smb_krb5_ctx_init(krb5_context *ctx)
{
if (krb5_init_context(ctx) != 0)
return (-1);
return (0);
}
/*
* Free the kerberos context.
*/
void
smb_krb5_ctx_fini(krb5_context ctx)
{
krb5_free_context(ctx);
}
/*
* Create an array of Kerberos Princiapls given an array of principal names.
* Caller must free the allocated memory using smb_krb5_free_kprincs()
* upon success.
*
* Returns 0 on success. Otherwise, returns -1.
*/
int
smb_krb5_get_kprincs(krb5_context ctx, char **names, size_t num,
krb5_principal **krb5princs)
{
int i;
if ((*krb5princs = calloc(num, sizeof (krb5_principal *))) == NULL) {
return (-1);
}
for (i = 0; i < num; i++) {
if (krb5_parse_name(ctx, names[i], &(*krb5princs)[i]) != 0) {
smb_krb5_free_kprincs(ctx, *krb5princs, i);
return (-1);
}
}
return (0);
}
void
smb_krb5_free_kprincs(krb5_context ctx, krb5_principal *krb5princs,
size_t num)
{
int i;
for (i = 0; i < num; i++)
krb5_free_principal(ctx, krb5princs[i]);
free(krb5princs);
}
/*
* Set the workstation trust account password.
* Returns 0 on success. Otherwise, returns non-zero value.
*/
int
smb_krb5_setpwd(krb5_context ctx, const char *fqdn, char *passwd)
{
krb5_error_code code;
krb5_ccache cc = NULL;
int result_code = 0;
krb5_data result_code_string, result_string;
krb5_principal princ;
char msg[SMB_KRB5_MAX_BUFLEN];
if (smb_krb5_get_kprinc(ctx, SMB_KRB5_PN_ID_HOST_FQHN,
SMB_PN_UPN_ATTR, fqdn, &princ) != 0)
return (-1);
(void) memset(&result_code_string, 0, sizeof (result_code_string));
(void) memset(&result_string, 0, sizeof (result_string));
if ((code = krb5_cc_default(ctx, &cc)) != 0) {
(void) snprintf(msg, sizeof (msg), "smbns_ksetpwd: failed to "
"find %s", SMB_CCACHE_PATH);
smb_krb5_log_errmsg(ctx, msg, code);
krb5_free_principal(ctx, princ);
return (-1);
}
code = krb5_set_password_using_ccache(ctx, cc, passwd, princ,
&result_code, &result_code_string, &result_string);
(void) krb5_cc_close(ctx, cc);
if (code != 0)
smb_krb5_log_errmsg(ctx, "smbns_ksetpwd: KPASSWD protocol "
"exchange failed", code);
if (result_code != 0) {
syslog(LOG_ERR, "smbns_ksetpwd: KPASSWD failed: rc=%d %.*s",
result_code,
result_code_string.length,
result_code_string.data);
if (code == 0)
code = EACCES;
}
krb5_free_principal(ctx, princ);
free(result_code_string.data);
free(result_string.data);
return (code);
}
/*
* Open the keytab file for writing.
* The keytab should be closed by calling krb5_kt_close().
*/
static int
smb_krb5_kt_open(krb5_context ctx, char *fname, krb5_keytab *kt)
{
char *ktname;
krb5_error_code code;
int len;
char msg[SMB_KRB5_MAX_BUFLEN];
*kt = NULL;
len = snprintf(NULL, 0, "WRFILE:%s", fname) + 1;
if ((ktname = malloc(len)) == NULL) {
syslog(LOG_ERR, "smbns_ksetpwd: unable to open keytab %s: "
"possible transient memory shortage", fname);
return (-1);
}
(void) snprintf(ktname, len, "WRFILE:%s", fname);
if ((code = krb5_kt_resolve(ctx, ktname, kt)) != 0) {
(void) snprintf(msg, sizeof (msg), "smbns_ksetpwd: %s", fname);
smb_krb5_log_errmsg(ctx, msg, code);
free(ktname);
return (-1);
}
free(ktname);
return (0);
}
/*
* Populate the keytab with keys of the specified key version for the
* specified set of krb5 principals. All service keys will be salted by:
* host/<truncated@15_lower_case_hostname>.<fqdn>@<REALM>
*/
int
smb_krb5_kt_populate(krb5_context ctx, const char *fqdn,
krb5_principal *princs, int count, char *fname, krb5_kvno kvno,
char *passwd, krb5_enctype *enctypes, int enctype_count)
{
krb5_keytab kt = NULL;
krb5_data salt;
krb5_error_code code;
krb5_principal salt_princ;
int i, j;
if (smb_krb5_kt_open(ctx, fname, &kt) != 0)
return (-1);
if (smb_krb5_get_kprinc(ctx, SMB_KRB5_PN_ID_SALT, SMB_PN_SALT,
fqdn, &salt_princ) != 0) {
(void) krb5_kt_close(ctx, kt);
return (-1);
}
code = krb5_principal2salt(ctx, salt_princ, &salt);
if (code != 0) {
smb_krb5_log_errmsg(ctx, "smbns_ksetpwd: salt computation "
"failed", code);
krb5_free_principal(ctx, salt_princ);
(void) krb5_kt_close(ctx, kt);
return (-1);
}
for (j = 0; j < count; j++) {
for (i = 0; i < enctype_count; i++) {
if (smb_krb5_kt_addkey(ctx, kt, princs[j], enctypes[i],
kvno, &salt, passwd) != 0) {
krb5_free_principal(ctx, salt_princ);
krb5_xfree(salt.data);
(void) krb5_kt_close(ctx, kt);
return (-1);
}
}
}
krb5_free_principal(ctx, salt_princ);
krb5_xfree(salt.data);
(void) krb5_kt_close(ctx, kt);
return (0);
}
boolean_t
smb_krb5_kt_find(smb_krb5_pn_id_t id, const char *fqdn, char *fname)
{
krb5_context ctx;
krb5_keytab kt;
krb5_keytab_entry entry;
krb5_principal princ;
char ktname[MAXPATHLEN];
boolean_t found = B_FALSE;
if (!fqdn || !fname)
return (found);
if (smb_krb5_ctx_init(&ctx) != 0)
return (found);
if (smb_krb5_get_kprinc(ctx, id, SMB_PN_KEYTAB_ENTRY, fqdn,
&princ) != 0) {
smb_krb5_ctx_fini(ctx);
return (found);
}
(void) snprintf(ktname, MAXPATHLEN, "FILE:%s", fname);
if (krb5_kt_resolve(ctx, ktname, &kt) == 0) {
if (krb5_kt_get_entry(ctx, kt, princ, 0, 0, &entry) == 0) {
found = B_TRUE;
(void) krb5_kt_free_entry(ctx, &entry);
}
(void) krb5_kt_close(ctx, kt);
}
krb5_free_principal(ctx, princ);
smb_krb5_ctx_fini(ctx);
return (found);
}
/*
* Add a key of the specified encryption type for the specified principal
* to the keytab file.
* Returns 0 on success. Otherwise, returns -1.
*/
static int
smb_krb5_kt_addkey(krb5_context ctx, krb5_keytab kt, const krb5_principal princ,
krb5_enctype enctype, krb5_kvno kvno, const krb5_data *salt,
const char *pw)
{
krb5_keytab_entry *entry;
krb5_data password;
krb5_keyblock key;
krb5_error_code code;
char buf[SMB_KRB5_MAX_BUFLEN], msg[SMB_KRB5_MAX_BUFLEN];
int rc = 0;
if ((code = krb5_enctype_to_string(enctype, buf, sizeof (buf)))) {
(void) snprintf(msg, sizeof (msg), "smbns_ksetpwd: unknown "
"encryption type (%d)", enctype);
smb_krb5_log_errmsg(ctx, msg, code);
return (-1);
}
if ((entry = (krb5_keytab_entry *) malloc(sizeof (*entry))) == NULL) {
syslog(LOG_ERR, "smbns_ksetpwd: possible transient "
"memory shortage");
return (-1);
}
(void) memset((char *)entry, 0, sizeof (*entry));
password.length = strlen(pw);
password.data = (char *)pw;
code = krb5_c_string_to_key(ctx, enctype, &password, salt, &key);
if (code != 0) {
(void) snprintf(msg, sizeof (msg), "smbns_ksetpwd: failed to "
"generate key (%d)", enctype);
smb_krb5_log_errmsg(ctx, msg, code);
free(entry);
return (-1);
}
(void) memcpy(&entry->key, &key, sizeof (krb5_keyblock));
entry->vno = kvno;
entry->principal = princ;
if ((code = krb5_kt_add_entry(ctx, kt, entry)) != 0) {
(void) snprintf(msg, sizeof (msg), "smbns_ksetpwd: failed to "
"add key (%d)", enctype);
smb_krb5_log_errmsg(ctx, msg, code);
rc = -1;
}
free(entry);
if (key.length)
krb5_free_keyblock_contents(ctx, &key);
return (rc);
}
static int
smb_krb5_spn_count(uint32_t type)
{
int i, cnt;
for (i = 0, cnt = 0; i < SMB_KRB5_SPN_TAB_SZ; i++) {
if (smb_krb5_pn_tab[i].p_flags & type)
cnt++;
}
return (cnt);
}
/*
* Generate the Kerberos Principal given a principal name format and the
* fully qualified domain name. On success, caller must free the allocated
* memory by calling krb5_free_principal().
*/
static int
smb_krb5_get_kprinc(krb5_context ctx, smb_krb5_pn_id_t id, uint32_t type,
const char *fqdn, krb5_principal *princ)
{
char *buf;
if ((buf = smb_krb5_get_pn_by_id(id, type, fqdn)) == NULL)
return (-1);
if (krb5_parse_name(ctx, buf, princ) != 0) {
free(buf);
return (-1);
}
free(buf);
return (0);
}
/*
* Looks up an entry in the principal name table given the ID.
*/
static smb_krb5_pn_t *
smb_krb5_lookup_pn(smb_krb5_pn_id_t id)
{
int i;
smb_krb5_pn_t *tabent;
for (i = 0; i < SMB_KRB5_SPN_TAB_SZ; i++) {
tabent = &smb_krb5_pn_tab[i];
if (id == tabent->p_id)
return (tabent);
}
return (NULL);
}
/*
* Construct the principal name given an ID, the requested type, and the
* fully-qualified name of the domain of which the principal is a member.
*/
static char *
smb_krb5_get_pn_by_id(smb_krb5_pn_id_t id, uint32_t type,
const char *fqdn)
{
char nbname[NETBIOS_NAME_SZ];
char hostname[MAXHOSTNAMELEN];
char *realm = NULL;
smb_krb5_pn_t *pn;
char *buf;
(void) smb_getnetbiosname(nbname, NETBIOS_NAME_SZ);
(void) smb_gethostname(hostname, MAXHOSTNAMELEN, SMB_CASE_LOWER);
pn = smb_krb5_lookup_pn(id);
/* detect inconsistent requested format and type */
if ((type & pn->p_flags) != type)
return (NULL);
switch (id) {
case SMB_KRB5_PN_ID_SALT:
(void) asprintf(&buf, "%s/%s.%s",
pn->p_svc, smb_strlwr(nbname), fqdn);
break;
case SMB_KRB5_PN_ID_HOST_FQHN:
case SMB_KRB5_PN_ID_CIFS_FQHN:
case SMB_KRB5_PN_ID_NFS_FQHN:
case SMB_KRB5_PN_ID_HTTP_FQHN:
case SMB_KRB5_PN_ID_ROOT_FQHN:
(void) asprintf(&buf, "%s/%s.%s",
pn->p_svc, hostname, fqdn);
break;
case SMB_KRB5_PN_ID_HOST_SHORT:
case SMB_KRB5_PN_ID_CIFS_SHORT:
(void) asprintf(&buf, "%s/%s",
pn->p_svc, nbname);
break;
/*
* SPN for the machine account, which is simply the
* (short) machine name with a dollar sign appended.
*/
case SMB_KRB5_PN_ID_MACHINE:
(void) asprintf(&buf, "%s$", nbname);
break;
default:
return (NULL);
}
/*
* If the requested principal is either added to keytab / the machine
* account as the UPN attribute or used for key salt generation,
* the principal name must have the @<REALM> portion.
*/
if (type & (SMB_PN_KEYTAB_ENTRY | SMB_PN_UPN_ATTR | SMB_PN_SALT)) {
if ((realm = strdup(fqdn)) == NULL) {
free(buf);
return (NULL);
}
(void) smb_strupr(realm);
if (buf != NULL) {
char *tmp;
(void) asprintf(&tmp, "%s@%s", buf,
realm);
free(buf);
buf = tmp;
}
free(realm);
}
return (buf);
}