From eeece1a027ff6fd74e73d6dbbd9f02a421d3850b Mon Sep 17 00:00:00 2001 From: fifthdegree Date: Mon, 6 Jun 2022 23:30:42 -0400 Subject: [PATCH] server-side kerberos (and some fixes) --- include/freerdp/settings.h | 4 +- libfreerdp/common/settings_getters.c | 9 + libfreerdp/common/settings_str.c | 1 + libfreerdp/core/nla.c | 15 +- .../core/test/settings_property_lists.h | 1 + server/shadow/shadow.c | 2 + server/shadow/shadow_server.c | 4 + winpr/include/winpr/sspi.h | 1 + winpr/libwinpr/sspi/Kerberos/kerberos.c | 171 +++++++++++++++--- winpr/libwinpr/sspi/Negotiate/negotiate.c | 51 ++++-- 10 files changed, 207 insertions(+), 52 deletions(-) diff --git a/include/freerdp/settings.h b/include/freerdp/settings.h index 28edb047b..d5d05602c 100644 --- a/include/freerdp/settings.h +++ b/include/freerdp/settings.h @@ -680,6 +680,7 @@ typedef struct #define FreeRDP_KerberosRenewableLifeTime (1348) #define FreeRDP_KerberosCache (1349) #define FreeRDP_KerberosArmor (1350) +#define FreeRDP_KerberosKeytab (1351) #define FreeRDP_IgnoreCertificate (1408) #define FreeRDP_CertificateName (1409) #define FreeRDP_CertificateFile (1410) @@ -1186,7 +1187,8 @@ struct rdp_settings ALIGN64 char* KerberosRenewableLifeTime; /* 1348 */ ALIGN64 char* KerberosCache; /* 1349 */ ALIGN64 char* KerberosArmor; /* 1350 */ - UINT64 padding1408[1408 - 1351]; /* 1351 */ + ALIGN64 char* KerberosKeytab; /* 1351 */ + UINT64 padding1408[1408 - 1352]; /* 1352 */ /* Server Certificate */ ALIGN64 BOOL IgnoreCertificate; /* 1408 */ diff --git a/libfreerdp/common/settings_getters.c b/libfreerdp/common/settings_getters.c index 644a32b51..f8f380f5a 100644 --- a/libfreerdp/common/settings_getters.c +++ b/libfreerdp/common/settings_getters.c @@ -2454,6 +2454,9 @@ const char* freerdp_settings_get_string(const rdpSettings* settings, size_t id) case FreeRDP_KerberosKdc: return settings->KerberosKdc; + case FreeRDP_KerberosKeytab: + return settings->KerberosKeytab; + case FreeRDP_KerberosLifeTime: return settings->KerberosLifeTime; @@ -2718,6 +2721,9 @@ char* freerdp_settings_get_string_writable(rdpSettings* settings, size_t id) case FreeRDP_KerberosKdc: return settings->KerberosKdc; + case FreeRDP_KerberosKeytab: + return settings->KerberosKeytab; + case FreeRDP_KerberosLifeTime: return settings->KerberosLifeTime; @@ -2992,6 +2998,9 @@ BOOL freerdp_settings_set_string_(rdpSettings* settings, size_t id, const char* case FreeRDP_KerberosKdc: return update_string(&settings->KerberosKdc, cnv.cc, len, cleanup); + case FreeRDP_KerberosKeytab: + return update_string(&settings->KerberosKeytab, cnv.cc, len, cleanup); + case FreeRDP_KerberosLifeTime: return update_string(&settings->KerberosLifeTime, cnv.cc, len, cleanup); diff --git a/libfreerdp/common/settings_str.c b/libfreerdp/common/settings_str.c index 5d7ff4aa4..a9f8fa0b8 100644 --- a/libfreerdp/common/settings_str.c +++ b/libfreerdp/common/settings_str.c @@ -347,6 +347,7 @@ static const struct settings_str_entry settings_map[] = { { FreeRDP_KerberosArmor, 7, "FreeRDP_KerberosArmor" }, { FreeRDP_KerberosCache, 7, "FreeRDP_KerberosCache" }, { FreeRDP_KerberosKdc, 7, "FreeRDP_KerberosKdc" }, + { FreeRDP_KerberosKeytab, 7, "FreeRDP_KerberosKeytab" }, { FreeRDP_KerberosLifeTime, 7, "FreeRDP_KerberosLifeTime" }, { FreeRDP_KerberosRealm, 7, "FreeRDP_KerberosRealm" }, { FreeRDP_KerberosRenewableLifeTime, 7, "FreeRDP_KerberosRenewableLifeTime" }, diff --git a/libfreerdp/core/nla.c b/libfreerdp/core/nla.c index 2a2f041be..304c8f37b 100644 --- a/libfreerdp/core/nla.c +++ b/libfreerdp/core/nla.c @@ -943,6 +943,16 @@ static BOOL nla_setup_kerberos(rdpNla* nla) } } + if (settings->KerberosKeytab) + { + kerbSettings->keytab = _strdup(settings->KerberosKeytab); + if (!kerbSettings->keytab) + { + WLog_ERR(TAG, "unable to copy keytab name"); + return FALSE; + } + } + if (settings->KerberosArmor) { kerbSettings->armorCache = _strdup(settings->KerberosArmor); @@ -1352,13 +1362,16 @@ static int nla_server_init(rdpNla* nla) if (!nla_sspi_module_init(nla)) return -1; + if (!nla_setup_kerberos(nla)) + return -1; + nla->status = nla_update_package_name(nla); if (nla->status != SEC_E_OK) return -1; nla->status = - nla->table->AcquireCredentialsHandle(NULL, NLA_PKG_NAME, SECPKG_CRED_INBOUND, NULL, NULL, + nla->table->AcquireCredentialsHandle(NULL, NLA_PKG_NAME, SECPKG_CRED_INBOUND, NULL, nla->identity, NULL, NULL, &nla->credentials, &nla->expiration); if (nla->status != SEC_E_OK) diff --git a/libfreerdp/core/test/settings_property_lists.h b/libfreerdp/core/test/settings_property_lists.h index fd827c3b6..ef7195eb5 100644 --- a/libfreerdp/core/test/settings_property_lists.h +++ b/libfreerdp/core/test/settings_property_lists.h @@ -356,6 +356,7 @@ static const size_t string_list_indices[] = { FreeRDP_KerberosArmor, FreeRDP_KerberosCache, FreeRDP_KerberosKdc, + FreeRDP_KerberosKeytab, FreeRDP_KerberosLifeTime, FreeRDP_KerberosRealm, FreeRDP_KerberosRenewableLifeTime, diff --git a/server/shadow/shadow.c b/server/shadow/shadow.c index c11ae1e59..328e3c05e 100644 --- a/server/shadow/shadow.c +++ b/server/shadow/shadow.c @@ -71,6 +71,8 @@ int main(int argc, char** argv) "nla extended protocol security" }, { "sam-file", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, "NTLM SAM file for NLA authentication" }, + { "keytab", COMMAND_LINE_VALUE_REQUIRED, "", NULL, NULL, -1, NULL, + "Kerberos keytab file for NLA authentication" }, { "gfx-progressive", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, "Allow GFX progressive codec" }, { "gfx-rfx", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueTrue, NULL, -1, NULL, diff --git a/server/shadow/shadow_server.c b/server/shadow/shadow_server.c index 7340d5c7f..9634fdc9a 100644 --- a/server/shadow/shadow_server.c +++ b/server/shadow/shadow_server.c @@ -392,6 +392,10 @@ int shadow_server_parse_command_line(rdpShadowServer* server, int argc, char** a if (!freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444, arg->Value ? TRUE : FALSE)) return COMMAND_LINE_ERROR; } + CommandLineSwitchCase(arg, "keytab") + { + freerdp_settings_set_string(settings, FreeRDP_KerberosKeytab, arg->Value); + } CommandLineSwitchDefault(arg) { } diff --git a/winpr/include/winpr/sspi.h b/winpr/include/winpr/sspi.h index 5062cc4aa..bfdcfc6d0 100644 --- a/winpr/include/winpr/sspi.h +++ b/winpr/include/winpr/sspi.h @@ -601,6 +601,7 @@ typedef struct typedef struct { + char* keytab; char* cache; char* armorCache; char* pkinitX509Anchors; diff --git a/winpr/libwinpr/sspi/Kerberos/kerberos.c b/winpr/libwinpr/sspi/Kerberos/kerberos.c index 0f9001c5f..8016b67cd 100644 --- a/winpr/libwinpr/sspi/Kerberos/kerberos.c +++ b/winpr/libwinpr/sspi/Kerberos/kerberos.c @@ -169,21 +169,23 @@ static SECURITY_STATUS SEC_ENTRY kerberos_AcquireCredentialsHandleA( krb5_error_code rv; krb5_context ctx = NULL; krb5_ccache ccache = NULL; + krb5_keytab keytab = NULL; krb5_get_init_creds_opt* gic_opt = NULL; krb5_deltat start_time = 0; krb5_principal principal = NULL; + krb5_principal cache_principal = NULL; krb5_creds creds; BOOL is_unicode = FALSE; char* domain = NULL; char* username = NULL; char* password = NULL; char* ccache_name = NULL; + char keytab_name[PATH_MAX]; sspi_gss_OID_set_desc desired_mechs = { 1, SSPI_GSS_C_SPNEGO_KRB5 }; - sspi_gss_key_value_element_desc ccache_setting = { "ccache", ccache_name }; - sspi_gss_key_value_set_desc cred_store = { 1, &ccache_setting }; + sspi_gss_key_value_element_desc cred_store_opts[2]; + sspi_gss_key_value_set_desc cred_store = { 2, cred_store_opts }; sspi_gss_cred_id_t gss_creds = NULL; OM_uint32 major, minor; - char* fallback_cc_name = "MEMORY:"; int cred_usage; switch (fCredentialUse) @@ -218,19 +220,14 @@ static SECURITY_STATUS SEC_ENTRY kerberos_AcquireCredentialsHandleA( username = (char*)identity->User; password = (char*)identity->Password; } + + if (!pszPrincipal) + pszPrincipal = username; } if ((rv = krb5_init_context(&ctx))) goto cleanup; - /* If user provided a cache use it whether or not it's initialized with the right principal */ - if (krb_settings && krb_settings->cache) - { - if ((rv = krb5_cc_set_default_name(ctx, krb_settings->cache))) - goto cleanup; - fallback_cc_name = krb_settings->cache; - } - if (domain) { CharUpperA(domain); @@ -239,36 +236,67 @@ static SECURITY_STATUS SEC_ENTRY kerberos_AcquireCredentialsHandleA( goto cleanup; } - if (username) + if (pszPrincipal) { /* Find realm component if included and convert to uppercase */ - char* p = username; - for (; *p != '@' && *p != 0; p++) - ; + char *p = strchr(pszPrincipal, '@'); CharUpperA(p); - if ((rv = krb5_parse_name(ctx, username, &principal))) + if ((rv = krb5_parse_name(ctx, pszPrincipal, &principal))) goto cleanup; } - else + + if (krb_settings && krb_settings->cache) { + if ((rv = krb5_cc_resolve(ctx, krb_settings->cache, &ccache))) + goto cleanup; + + /* Make sure the cache is initialized with the right principal */ + if (principal) + { + if ((rv = krb5_cc_get_principal(ctx, ccache, &cache_principal))) + goto cleanup; + if (!krb5_principal_compare(ctx, principal, cache_principal)) + if ((rv = krb5_cc_initialize(ctx, ccache, principal))) + goto cleanup; + } + } + else if (principal) + { + /* Use the default cache if it's initialized with the right principal */ + if (krb5_cc_cache_match(ctx, principal, &ccache) == KRB5_CC_NOTFOUND) + { + if ((rv = krb5_cc_resolve(ctx, "MEMORY:", &ccache))) + goto cleanup; + if ((rv = krb5_cc_initialize(ctx, ccache, principal))) + goto cleanup; + } + } + else if (fCredentialUse & SECPKG_CRED_OUTBOUND) + { + /* Use the default cache with it's default principal */ if ((rv = krb5_cc_default(ctx, &ccache))) goto cleanup; if ((rv = krb5_cc_get_principal(ctx, ccache, &principal))) goto cleanup; } - - /* If the default (or user provided) cache is already initialized with the right principal use - it otherwise initialize a new cache in memory (or the user provided one) with our principal - */ - if (krb5_cc_cache_match(ctx, principal, &ccache) == KRB5_CC_NOTFOUND) + else { - if ((rv = krb5_cc_resolve(ctx, fallback_cc_name, &ccache))) - goto cleanup; - if ((rv = krb5_cc_initialize(ctx, ccache, principal))) + if ((rv = krb5_cc_resolve(ctx, "MEMORY:", &ccache))) goto cleanup; } + if (krb_settings && krb_settings->keytab) + { + if ((rv = krb5_kt_resolve(ctx, krb_settings->keytab, &keytab))) + goto cleanup; + } + else + { + if ((rv = krb5_kt_default(ctx, &keytab))) + goto cleanup; + } + if ((rv = krb5_get_init_creds_opt_alloc(ctx, &gic_opt))) goto cleanup; @@ -301,15 +329,22 @@ static SECURITY_STATUS SEC_ENTRY kerberos_AcquireCredentialsHandleA( #endif krb5_cc_get_full_name(ctx, ccache, &ccache_name); - ccache_setting.value = ccache_name; + krb5_kt_get_name(ctx, keytab, keytab_name, PATH_MAX); + + cred_store_opts[0].key = "ccache"; + cred_store_opts[0].value = ccache_name; + cred_store_opts[1].key = "keytab"; + cred_store_opts[1].value = keytab_name; /* Check if there are initial creds already in the cache there's no need to request new ones */ major = sspi_gss_acquire_cred_from(&minor, SSPI_GSS_C_NO_NAME, SSPI_GSS_C_INDEFINITE, - &desired_mechs, SSPI_GSS_C_INITIATE, &cred_store, &gss_creds, + &desired_mechs, cred_usage, &cred_store, &gss_creds, NULL, NULL); if (major != SSPI_GSS_S_NO_CRED) goto cleanup; + gss_log_status_messages(major, minor); + if ((rv = krb5_get_init_creds_password(ctx, &creds, principal, password, krb5_prompter, password, start_time, NULL, gic_opt))) goto cleanup; @@ -343,7 +378,7 @@ cleanup: if (password) free(password); } - if (ccache_setting.value) + if (ccache_name) krb5_free_string(ctx, ccache_name); if (principal) krb5_free_principal(ctx, principal); @@ -556,6 +591,82 @@ static SECURITY_STATUS SEC_ENTRY kerberos_InitializeSecurityContextW( return status; } +static SECURITY_STATUS SEC_ENTRY kerberos_AcceptSecurityContext( + PCredHandle phCredential, PCtxtHandle phContext, PSecBufferDesc pInput, ULONG fContextReq, + ULONG TargetDataRep, PCtxtHandle phNewContext, PSecBufferDesc pOutput, ULONG *pfContextAttr, PTimeStamp ptsExpity) +{ +#ifdef WITH_GSSAPI + KRB_CONTEXT *context; + sspi_gss_cred_id_t creds; + sspi_gss_ctx_id_t gss_ctx = SSPI_GSS_C_NO_CONTEXT; + PSecBuffer input_buffer; + PSecBuffer output_buffer = NULL; + sspi_gss_buffer_desc input_token; + sspi_gss_buffer_desc output_token; + UINT32 major, minor; + UINT32 time_rec; + + context = sspi_SecureHandleGetLowerPointer(phContext); + creds = sspi_SecureHandleGetLowerPointer(phCredential); + + if (pInput) + input_buffer = sspi_FindSecBuffer(pInput, SECBUFFER_TOKEN); + if (pOutput) + output_buffer = sspi_FindSecBuffer(pOutput, SECBUFFER_TOKEN); + + if (!input_buffer) + return SEC_E_INVALID_TOKEN; + + if (context) + gss_ctx = context->gss_ctx; + + input_token.length = input_buffer->cbBuffer; + input_token.value = input_buffer->pvBuffer; + + major = sspi_gss_accept_sec_context( + &minor, &gss_ctx, creds, &input_token, SSPI_GSS_C_NO_CHANNEL_BINDINGS, NULL, NULL, &output_token, pfContextAttr, &time_rec, NULL); + + if (SSPI_GSS_ERROR(major)) + { + gss_log_status_messages(major, minor); + return SEC_E_INTERNAL_ERROR; + } + + if (output_token.length > 0) + { + if (output_buffer && output_buffer->cbBuffer >= output_token.length) + { + output_buffer->cbBuffer = output_token.length; + CopyMemory(output_buffer->pvBuffer, output_token.value, output_token.length); + sspi_gss_release_buffer(&minor, &output_token); + } + else + { + sspi_gss_release_buffer(&minor, &output_token); + return SEC_E_INVALID_TOKEN; + } + } + + if (!context) + { + context = kerberos_ContextNew(); + if (!context) + return SEC_E_INSUFFICIENT_MEMORY; + } + + context->gss_ctx = gss_ctx; + sspi_SecureHandleSetUpperPointer(phNewContext, KERBEROS_SSP_NAME); + sspi_SecureHandleSetLowerPointer(phNewContext, context); + + if (major & SSPI_GSS_S_CONTINUE_NEEDED) + return SEC_I_CONTINUE_NEEDED; + + return SEC_E_OK; +#else + return SEC_E_UNSUPPORTED_FUNCTION; +#endif /* WITH_GSSAPI */ +} + static SECURITY_STATUS SEC_ENTRY kerberos_DeleteSecurityContext(PCtxtHandle phContext) { KRB_CONTEXT* context; @@ -852,7 +963,7 @@ const SecurityFunctionTableA KERBEROS_SecurityFunctionTableA = { kerberos_FreeCredentialsHandle, /* FreeCredentialsHandle */ NULL, /* Reserved2 */ kerberos_InitializeSecurityContextA, /* InitializeSecurityContext */ - NULL, /* AcceptSecurityContext */ + kerberos_AcceptSecurityContext, /* AcceptSecurityContext */ NULL, /* CompleteAuthToken */ kerberos_DeleteSecurityContext, /* DeleteSecurityContext */ NULL, /* ApplyControlToken */ @@ -883,7 +994,7 @@ const SecurityFunctionTableW KERBEROS_SecurityFunctionTableW = { kerberos_FreeCredentialsHandle, /* FreeCredentialsHandle */ NULL, /* Reserved2 */ kerberos_InitializeSecurityContextW, /* InitializeSecurityContext */ - NULL, /* AcceptSecurityContext */ + kerberos_AcceptSecurityContext, /* AcceptSecurityContext */ NULL, /* CompleteAuthToken */ kerberos_DeleteSecurityContext, /* DeleteSecurityContext */ NULL, /* ApplyControlToken */ diff --git a/winpr/libwinpr/sspi/Negotiate/negotiate.c b/winpr/libwinpr/sspi/Negotiate/negotiate.c index 481a1f952..7cb664931 100644 --- a/winpr/libwinpr/sspi/Negotiate/negotiate.c +++ b/winpr/libwinpr/sspi/Negotiate/negotiate.c @@ -55,8 +55,8 @@ struct Mech_st { const sspi_gss_OID_desc* oid; const SecPkg* pkg; - const UINT init_flags; - const UINT accept_flags; + const UINT flags; + const BOOL preferred; }; typedef struct @@ -101,8 +101,8 @@ static const SecPkg SecPkgTable[] = { }; static const Mech MechTable[] = { - { &kerberos_OID, &SecPkgTable[0], 0, 0 }, - { &ntlm_OID, &SecPkgTable[1], 0, 0 }, + { &kerberos_OID, &SecPkgTable[0], 0, TRUE }, + { &ntlm_OID, &SecPkgTable[1], 0, FALSE }, }; static const int MECH_COUNT = sizeof(MechTable) / sizeof(Mech); @@ -671,7 +671,7 @@ static SECURITY_STATUS SEC_ENTRY negotiate_InitializeSecurityContextW( CopyMemory(&output_token.mechToken, output_buffer, sizeof(SecBuffer)); status = MechTable[i].pkg->table_w->InitializeSecurityContextW( - &creds[i].cred, NULL, pszTargetName, fContextReq | creds[i].mech->init_flags, + &creds[i].cred, NULL, pszTargetName, fContextReq | creds[i].mech->flags, Reserved1, TargetDataRep, NULL, Reserved2, &init_context.sub_context, &mech_output, pfContextAttr, ptsExpiry); @@ -802,7 +802,7 @@ static SECURITY_STATUS SEC_ENTRY negotiate_InitializeSecurityContextW( CopyMemory(&output_token.mechToken, output_buffer, sizeof(SecBuffer)); status = context->mech->pkg->table_w->InitializeSecurityContextW( - sub_cred, sub_context, pszTargetName, fContextReq | context->mech->init_flags, + sub_cred, sub_context, pszTargetName, fContextReq | context->mech->flags, Reserved1, TargetDataRep, input_token.mechToken.cbBuffer ? &mech_input : NULL, Reserved2, &context->sub_context, &mech_output, pfContextAttr, ptsExpiry); @@ -894,6 +894,7 @@ static SECURITY_STATUS SEC_ENTRY negotiate_AcceptSecurityContext( BYTE *p, tag; size_t bytes_remain, len; sspi_gss_OID_desc oid = { 0 }; + const Mech* first_mech = NULL; if (!phCredential || !SecIsValidHandle(phCredential)) return SEC_E_NO_CREDENTIALS; @@ -971,6 +972,8 @@ static SECURITY_STATUS SEC_ENTRY negotiate_AcceptSecurityContext( oid.length = len; oid.elements = p; + p += len; + bytes_remain -= len; init_context.mech = negotiate_GetMechByOID(oid); @@ -1009,7 +1012,9 @@ static SECURITY_STATUS SEC_ENTRY negotiate_AcceptSecurityContext( return status; init_context.mic = TRUE; + first_mech = init_context.mech; init_context.mech = NULL; + output_token.mechToken.cbBuffer = 0; } while (!init_context.mech && bytes_remain > 0) @@ -1021,8 +1026,15 @@ static SECURITY_STATUS SEC_ENTRY negotiate_AcceptSecurityContext( oid.length = len; oid.elements = p; + p += len; + bytes_remain -= len; init_context.mech = negotiate_GetMechByOID(oid); + + /* Microsoft may send two versions of the kerberos OID */ + if (init_context.mech == first_mech) + init_context.mech = NULL; + if (init_context.mech && !negotiate_FindCredential(creds, init_context.mech)) init_context.mech = NULL; } @@ -1047,22 +1059,21 @@ static SECURITY_STATUS SEC_ENTRY negotiate_AcceptSecurityContext( CopyMemory(init_context.mechTypes.pvBuffer, input_token.mechTypes.pvBuffer, input_token.mechTypes.cbBuffer); - /* Check if the chosen mechanism is our most preferred; otherwise request mic */ - for (int i = 0; i < MECH_COUNT; i++) + if (!context->mech->preferred) { - if (context->mech != creds[i].mech) - { - output_token.negState = REQUEST_MIC; - context->mic = TRUE; - } - else - { - output_token.negState = ACCEPT_INCOMPLETE; - } - break; + output_token.negState = REQUEST_MIC; + context->mic = TRUE; + } + else + { + output_token.negState = ACCEPT_INCOMPLETE; } - context->state = NEGOTIATE_STATE_NEGORESP; + if (status == SEC_E_OK) + context->state = NEGOTIATE_STATE_FINAL; + else + context->state = NEGOTIATE_STATE_NEGORESP; + output_token.supportedMech.length = oid.length; output_token.supportedMech.elements = oid.elements; WLog_DBG(TAG, "Accepted mechanism: %s", negotiate_mech_name(&output_token.supportedMech)); @@ -1094,7 +1105,7 @@ static SECURITY_STATUS SEC_ENTRY negotiate_AcceptSecurityContext( status = context->mech->pkg->table->AcceptSecurityContext( sub_cred, &context->sub_context, &mech_input, - fContextReq | context->mech->accept_flags, TargetDataRep, &context->sub_context, + fContextReq | context->mech->flags, TargetDataRep, &context->sub_context, pOutput, pfContextAttr, ptsTimeStamp); if (IsSecurityStatusError(status))