diff --git a/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs b/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs index fdb2133b300a..43873db4cc37 100644 --- a/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs +++ b/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs @@ -75,6 +75,14 @@ internal static extern Status InitSecContext( ref GssBuffer token, out uint retFlags); + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_AcceptSecContext")] + internal static extern Status AcceptSecContext( + out Status minorStatus, + ref SafeGssContextHandle acceptContextHandle, + byte[] inputBytes, + int inputLength, + ref GssBuffer token); + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_DeleteSecContext")] internal static extern Status DeleteSecContext( out Status minorStatus, diff --git a/src/Native/System.Net.Security.Native/pal_gssapi.cpp b/src/Native/System.Net.Security.Native/pal_gssapi.cpp index 1053dfd14e62..e3f3d4e87287 100644 --- a/src/Native/System.Net.Security.Native/pal_gssapi.cpp +++ b/src/Native/System.Net.Security.Native/pal_gssapi.cpp @@ -68,9 +68,8 @@ static uint32_t NetSecurityNative_AcquireCredSpNego(uint32_t* minorStatus, minorStatus, desiredName, 0, &gss_mech_spnego_OID_set_desc, credUsage, outputCredHandle, nullptr, nullptr); } -extern "C" uint32_t NetSecurityNative_InitiateCredSpNego(uint32_t* minorStatus, - GssName* desiredName, - GssCredId** outputCredHandle) +extern "C" uint32_t +NetSecurityNative_InitiateCredSpNego(uint32_t* minorStatus, GssName* desiredName, GssCredId** outputCredHandle) { return NetSecurityNative_AcquireCredSpNego(minorStatus, desiredName, GSS_C_INITIATE, outputCredHandle); } @@ -94,7 +93,7 @@ extern "C" uint32_t NetSecurityNative_DisplayStatus(uint32_t* minorStatus, int statusType = isGssMechCode ? GSS_C_MECH_CODE : GSS_C_GSS_CODE; uint32_t messageContext; - GssBuffer gssBuffer {.length = 0, .value = nullptr}; + GssBuffer gssBuffer{.length = 0, .value = nullptr}; uint32_t majorStatus = gss_display_status(minorStatus, statusValue, statusType, GSS_C_NO_OID, &messageContext, &gssBuffer); @@ -146,9 +145,9 @@ extern "C" uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus, assert(contextHandle != nullptr); assert(outBuffer != nullptr); assert(retFlags != nullptr); - assert (inputBytes != nullptr || inputLength == 0); + assert(inputBytes != nullptr || inputLength == 0); - //Note: claimantCredHandle can be null +// Note: claimantCredHandle can be null #if HAVE_GSS_SPNEGO_MECHANISM gss_OID desiredMech = isNtlm ? GSS_NTLM_MECHANISM : GSS_SPNEGO_MECHANISM; @@ -160,8 +159,8 @@ extern "C" uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus, gss_OID desiredMech = &gss_mech_spnego_OID_desc; #endif - GssBuffer inputToken {.length = UnsignedCast(inputLength), .value = inputBytes}; - GssBuffer gssBuffer { .length = 0, .value = nullptr }; + GssBuffer inputToken{.length = UnsignedCast(inputLength), .value = inputBytes}; + GssBuffer gssBuffer{.length = 0, .value = nullptr}; uint32_t majorStatus = gss_init_sec_context(minorStatus, claimantCredHandle, @@ -180,6 +179,35 @@ extern "C" uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus, return NetSecurityNative_HandleError(majorStatus, &gssBuffer, outBuffer); } +extern "C" uint32_t NetSecurityNative_AcceptSecContext(uint32_t* minorStatus, + GssCtxId** contextHandle, + uint8_t* inputBytes, + uint32_t inputLength, + struct PAL_GssBuffer* outBuffer) +{ + assert(minorStatus != nullptr); + assert(contextHandle != nullptr); + assert(outBuffer != nullptr); + assert(inputBytes != nullptr || inputLength == 0); + + GssBuffer inputToken{.length = UnsignedCast(inputLength), .value = inputBytes}; + GssBuffer gssBuffer{.length = 0, .value = nullptr}; + + uint32_t majorStatus = gss_accept_sec_context(minorStatus, + contextHandle, + GSS_C_NO_CREDENTIAL, + &inputToken, + GSS_C_NO_CHANNEL_BINDINGS, + nullptr, + nullptr, + &gssBuffer, + 0, + nullptr, + nullptr); + + return NetSecurityNative_HandleError(majorStatus, &gssBuffer, outBuffer); +} + extern "C" uint32_t NetSecurityNative_ReleaseCred(uint32_t* minorStatus, GssCredId** credHandle) { assert(minorStatus != nullptr); @@ -193,7 +221,7 @@ extern "C" void NetSecurityNative_ReleaseGssBuffer(void* buffer, uint64_t length assert(buffer != nullptr); uint32_t minorStatus; - GssBuffer gssBuffer {.length = length, .value = buffer}; + GssBuffer gssBuffer{.length = length, .value = buffer}; gss_release_buffer(&minorStatus, &gssBuffer); } @@ -227,8 +255,8 @@ extern "C" uint32_t NetSecurityNative_Wrap(uint32_t* minorStatus, int confState; GssBuffer inputMessageBuffer{.length = UnsignedCast(count), .value = inputBytes + offset}; GssBuffer gssBuffer; - uint32_t majorStatus = gss_wrap( - minorStatus, contextHandle, isEncrypt, GSS_C_QOP_DEFAULT, &inputMessageBuffer, &confState, &gssBuffer); + uint32_t majorStatus = + gss_wrap(minorStatus, contextHandle, isEncrypt, GSS_C_QOP_DEFAULT, &inputMessageBuffer, &confState, &gssBuffer); return NetSecurityNative_HandleError(majorStatus, &gssBuffer, outBuffer); } @@ -249,9 +277,8 @@ extern "C" uint32_t NetSecurityNative_Unwrap(uint32_t* minorStatus, // count refers to the length of the input message. That is, the number of bytes of inputBytes // starting at offset that need to be wrapped. GssBuffer inputMessageBuffer{.length = UnsignedCast(count), .value = inputBytes + offset}; - GssBuffer gssBuffer {.length = 0, .value = nullptr}; - uint32_t majorStatus = - gss_unwrap(minorStatus, contextHandle, &inputMessageBuffer, &gssBuffer, nullptr, nullptr); + GssBuffer gssBuffer{.length = 0, .value = nullptr}; + uint32_t majorStatus = gss_unwrap(minorStatus, contextHandle, &inputMessageBuffer, &gssBuffer, nullptr, nullptr); return NetSecurityNative_HandleError(majorStatus, &gssBuffer, outBuffer); } @@ -272,11 +299,9 @@ static uint32_t NetSecurityNative_AcquireCredWithPassword(uint32_t* minorStatus, minorStatus, desiredName, &passwordBuffer, 0, nullptr, credUsage, outputCredHandle, nullptr, nullptr); } -extern "C" uint32_t NetSecurityNative_InitiateCredWithPassword(uint32_t* minorStatus, - GssName* desiredName, - char* password, - uint32_t passwdLen, - GssCredId** outputCredHandle) +extern "C" uint32_t NetSecurityNative_InitiateCredWithPassword( + uint32_t* minorStatus, GssName* desiredName, char* password, uint32_t passwdLen, GssCredId** outputCredHandle) { - return NetSecurityNative_AcquireCredWithPassword(minorStatus, desiredName, password, passwdLen, GSS_C_INITIATE, outputCredHandle); + return NetSecurityNative_AcquireCredWithPassword( + minorStatus, desiredName, password, passwdLen, GSS_C_INITIATE, outputCredHandle); } diff --git a/src/Native/System.Net.Security.Native/pal_gssapi.h b/src/Native/System.Net.Security.Native/pal_gssapi.h index 9619196949b7..770535015665 100644 --- a/src/Native/System.Net.Security.Native/pal_gssapi.h +++ b/src/Native/System.Net.Security.Native/pal_gssapi.h @@ -80,9 +80,8 @@ extern "C" uint32_t NetSecurityNative_ReleaseName(uint32_t* minorStatus, GssName /* Shims the gss_acquire_cred method with SPNEGO oids with GSS_C_INITIATE */ -extern "C" uint32_t NetSecurityNative_InitiateCredSpNego(uint32_t* minorStatus, - GssName* desiredName, - GssCredId** outputCredHandle); +extern "C" uint32_t +NetSecurityNative_InitiateCredSpNego(uint32_t* minorStatus, GssName* desiredName, GssCredId** outputCredHandle); /* Shims the gss_release_cred method. @@ -104,6 +103,16 @@ extern "C" uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus, uint32_t* retFlags); /* +Shims the gss_accept_sec_context method +*/ +extern "C" uint32_t NetSecurityNative_AcceptSecContext(uint32_t* minorStatus, + GssCtxId** contextHandle, + uint8_t* inputBytes, + uint32_t inputLength, + struct PAL_GssBuffer* outBuffer); + +/* + Shims the gss_delete_sec_context method. */ extern "C" uint32_t NetSecurityNative_DeleteSecContext(uint32_t* minorStatus, GssCtxId** contextHandle); @@ -132,8 +141,5 @@ extern "C" uint32_t NetSecurityNative_Unwrap(uint32_t* minorStatus, /* Shims the gss_acquire_cred_with_password method with GSS_C_INITIATE */ -extern "C" uint32_t NetSecurityNative_InitiateCredWithPassword(uint32_t* minorStatus, - GssName* desiredName, - char* password, - uint32_t passwdLen, - GssCredId** outputCredHandle); +extern "C" uint32_t NetSecurityNative_InitiateCredWithPassword( + uint32_t* minorStatus, GssName* desiredName, char* password, uint32_t passwdLen, GssCredId** outputCredHandle); diff --git a/src/System.Net.Security/tests/FunctionalTests/KerberosTest.cs b/src/System.Net.Security/tests/FunctionalTests/KerberosTest.cs new file mode 100644 index 000000000000..abbc01182de9 --- /dev/null +++ b/src/System.Net.Security/tests/FunctionalTests/KerberosTest.cs @@ -0,0 +1,467 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Test.Common; +using System.Security.Authentication; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +using Xunit; + +namespace System.Net.Security.Tests +{ + public class KDCSetup : IDisposable + { + private const string Krb5ConfigFile = "/etc/krb5.conf"; + private const string KDestroyCmd = "kdestroy"; + private const string SudoCommand = "sudo"; + private const string ScriptName = "setup-kdc.sh"; + private const string ScriptUninstallArgs = "--uninstall --yes"; + private readonly bool _isKrbInstalled ; + + public KDCSetup() + { + _isKrbInstalled = File.Exists(Krb5ConfigFile); + if (!_isKrbInstalled) + { + int exitCode = RunSetupScript(); + if (exitCode != 0) + { + Dispose(); + Assert.True(false, "KDC setup failure"); + } + } + } + + public void Dispose() + { + if (!_isKrbInstalled) + { + RunSetupScript(ScriptUninstallArgs); + } + } + + // checks for avilability of Kerberos related infrastructure + // on the host. Returns true available, false otherwise + public bool CheckAndInitializeKerberos() + { + if (_isKrbInstalled) + { + // Clear the credentials + var startInfo = new ProcessStartInfo(KDestroyCmd); + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + startInfo.Arguments = "-A"; + using (Process clearCreds = Process.Start(startInfo)) + { + clearCreds.WaitForExit(); + return (clearCreds.ExitCode == 0); + } + } + + return false; + } + + private static int RunSetupScript(string args = null) + { + ProcessStartInfo startInfo = new ProcessStartInfo(); + + // since ProcessStartInfo does not support Verb, we use sudo as + // the program to be run + startInfo.FileName = SudoCommand; + startInfo.Arguments = string.Format("bash {0} {1}", ScriptName, args); + using (Process kdcSetup = Process.Start(startInfo)) + { + kdcSetup.WaitForExit(); + return kdcSetup.ExitCode; + } + } + } + + public class KerberosTest : IDisposable, IClassFixture + { + private readonly byte[] _firstMessage = Encoding.UTF8.GetBytes("Sample First Message"); + private readonly byte[] _secondMessage = Encoding.UTF8.GetBytes("Sample Second Message"); + private readonly bool _isKrbAvailable; // tests are no-op if kerberos is not available on the host machine + private readonly KDCSetup _fixture; + + public KerberosTest(KDCSetup fixture) + { + _fixture = fixture; + _isKrbAvailable = _fixture.CheckAndInitializeKerberos(); + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthentication_Success() + { + if (!_isKrbAvailable) + { + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated"); + + Task[] auth = new Task[2]; + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, TestConfiguration.Password); + auth[0] = client.AuthenticateAsClientAsync(credential, target); + auth[1] = server.AuthenticateAsServerAsync(); + + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + // Expected Client property values: + Assert.True(client.IsAuthenticated, "client is now authenticated"); + Assert.Equal(TokenImpersonationLevel.Identification, client.ImpersonationLevel); + Assert.True(client.IsEncrypted, "client is encrypted"); + Assert.True(client.IsMutuallyAuthenticated, "client is mutually authenticated"); + Assert.False(client.IsServer, "client is not server"); + Assert.True(client.IsSigned, "client is signed"); + Assert.False(client.LeaveInnerStreamOpen, "inner stream remains open"); + + IIdentity serverIdentity = client.RemoteIdentity; + Assert.Equal("Kerberos", serverIdentity.AuthenticationType); + Assert.True(serverIdentity.IsAuthenticated, "server identity is authenticated"); + IdentityValidator.AssertHasName(serverIdentity, target); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_AuthToHttpTarget_Success() + { + if (!_isKrbAvailable) + { + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated); + + Task[] auth = new Task[2]; + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}",TestConfiguration.HttpTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, TestConfiguration.Password); + auth[0] = client.AuthenticateAsClientAsync(credential, target); + auth[1] = server.AuthenticateAsServerAsync(); + + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + // Expected Client property values: + Assert.True(client.IsAuthenticated, "client is authenticated"); + Assert.Equal(TokenImpersonationLevel.Identification, client.ImpersonationLevel); + Assert.True(client.IsEncrypted, "client is encrypted"); + Assert.True(client.IsMutuallyAuthenticated, "mutually authentication is true"); + Assert.False(client.IsServer, "client is not a server"); + Assert.True(client.IsSigned, "clientStream is signed"); + Assert.False(client.LeaveInnerStreamOpen, "Inner stream is open"); + + IIdentity serverIdentity = client.RemoteIdentity; + Assert.Equal("Kerberos", serverIdentity.AuthenticationType); + Assert.True(serverIdentity.IsAuthenticated, "remote identity of client is authenticated"); + IdentityValidator.AssertHasName(serverIdentity, target); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthWithoutRealm_Success() + { + if (!_isKrbAvailable) + { + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated); + + Task[] auth = new Task[2]; + NetworkCredential credential = new NetworkCredential(TestConfiguration.KerberosUser, TestConfiguration.Password); + auth[0] = client.AuthenticateAsClientAsync(credential, TestConfiguration.HostTarget); + auth[1] = server.AuthenticateAsServerAsync(); + + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + // Expected Client property values: + Assert.True(client.IsAuthenticated, "client is authenticated"); + Assert.Equal(TokenImpersonationLevel.Identification, client.ImpersonationLevel); + Assert.True(client.IsEncrypted, "client stream is encrypted"); + Assert.True(client.IsMutuallyAuthenticated, "mutual authentication is true"); + Assert.False(client.IsServer, "client is not server"); + Assert.True(client.IsSigned, "client stream is signed"); + Assert.False(client.LeaveInnerStreamOpen, "inner stream is open"); + + IIdentity serverIdentity = client.RemoteIdentity; + Assert.Equal("Kerberos", serverIdentity.AuthenticationType); + Assert.True(serverIdentity.IsAuthenticated, "remote identity is authenticated"); + IdentityValidator.AssertHasName(serverIdentity, TestConfiguration.HostTarget); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthDefaultCredentials_Success() + { + if (!_isKrbAvailable) + { + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated before AuthenticateAsClient call"); + + Task[] auth = new Task[2]; + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + // Seed the default Kerberos cache with the TGT + UnixGssFakeNegotiateStream.GetDefaultKerberosCredentials(user, TestConfiguration.Password); + auth[0] = client.AuthenticateAsClientAsync(CredentialCache.DefaultNetworkCredentials, target); + auth[1] = server.AuthenticateAsServerAsync(); + + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + // Expected Client property values: + Assert.True(client.IsAuthenticated, "client is now authenticated"); + Assert.Equal(TokenImpersonationLevel.Identification, client.ImpersonationLevel); + Assert.True(client.IsEncrypted, "client stream is encrypted"); + Assert.True(client.IsMutuallyAuthenticated, "mutual authentication is true"); + Assert.False(client.IsServer, "client is not server"); + Assert.True(client.IsSigned, "client stream is signed"); + Assert.False(client.LeaveInnerStreamOpen, "inner stream is open"); + + IIdentity serverIdentity = client.RemoteIdentity; + Assert.Equal("Kerberos", serverIdentity.AuthenticationType); + Assert.True(serverIdentity.IsAuthenticated,"server identity is authenticated"); + IdentityValidator.AssertHasName(serverIdentity, target); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_EchoServer_ClientWriteRead_Successive_Sync_Success() + { + if (!_isKrbAvailable) + { + return; + } + + VirtualNetwork network = new VirtualNetwork(); + byte[] firstRecvBuffer = new byte[_firstMessage.Length]; + byte[] secondRecvBuffer = new byte[_secondMessage.Length]; + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated before AuthenticateAsClient call"); + + Task[] auth = new Task[2]; + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, TestConfiguration.Password); + auth[0] = client.AuthenticateAsClientAsync(credential, target); + auth[1] = server.AuthenticateAsServerAsync(); + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + Task svrMsgTask = server.PollMessageAsync(2); + + client.Write(_firstMessage, 0, _firstMessage.Length); + client.Write(_secondMessage, 0, _secondMessage.Length); + client.Read(firstRecvBuffer, 0, firstRecvBuffer.Length); + client.Read(secondRecvBuffer, 0, secondRecvBuffer.Length); + Assert.True(_firstMessage.SequenceEqual(firstRecvBuffer), "first message received is as expected"); + Assert.True(_secondMessage.SequenceEqual(secondRecvBuffer), "second message received is as expected"); + finished = svrMsgTask.Wait(TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Message roundtrip completed in the allotted time"); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_EchoServer_ClientWriteRead_Successive_Async_Success() + { + if (!_isKrbAvailable) + { + return; + } + + VirtualNetwork network = new VirtualNetwork(); + byte[] firstRecvBuffer = new byte[_firstMessage.Length]; + byte[] secondRecvBuffer = new byte[_secondMessage.Length]; + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated before AuthenticateAsClient call"); + + Task[] auth = new Task[2]; + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, TestConfiguration.Password); + auth[0] = client.AuthenticateAsClientAsync(credential, target); + auth[1] = server.AuthenticateAsServerAsync(); + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + Task serverTask = server.PollMessageAsync(2); + Task[] msgTasks = new Task[5]; + msgTasks[0] = client.WriteAsync(_firstMessage, 0, _firstMessage.Length); + msgTasks[1] = client.WriteAsync(_secondMessage, 0, _secondMessage.Length); + msgTasks[2] = client.ReadAsync(firstRecvBuffer, 0, firstRecvBuffer.Length); + msgTasks[3] = client.ReadAsync(secondRecvBuffer, 0, secondRecvBuffer.Length); + msgTasks[4] = serverTask; + finished = Task.WaitAll(msgTasks, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Messages sent and received in the allotted time"); + Assert.True(_firstMessage.SequenceEqual(firstRecvBuffer), "The first message received is as expected"); + Assert.True(_secondMessage.SequenceEqual(secondRecvBuffer), "The second message received is as expected"); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthDefaultCredentialsNoSeed_Failure() + { + if (!_isKrbAvailable) + { + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var client = new NegotiateStream(clientStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated before AuthenticateAsClient call"); + + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + Assert.ThrowsAsync(() => client.AuthenticateAsClientAsync(CredentialCache.DefaultNetworkCredentials, target)); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthInvalidUser_Failure() + { + if (!_isKrbAvailable) + { + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var client = new NegotiateStream(clientStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated by default"); + + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user.Substring(1), TestConfiguration.Password); + Assert.Throws(() => + { + client.AuthenticateAsClientAsync(credential, target); + }); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthInvalidPassword_Failure() + { + if (!_isKrbAvailable) + { + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var client = new NegotiateStream(clientStream)) + { + Assert.False(client.IsAuthenticated, "client stream is not authenticated by default"); + + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, TestConfiguration.Password.Substring(1)); + Assert.Throws(() => + { + client.AuthenticateAsClientAsync(credential, target); + }); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthInvalidTarget_Failure() + { + if (!_isKrbAvailable) + { + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var client = new NegotiateStream(clientStream)) + { + Assert.False(client.IsAuthenticated, "client stream is not authenticated by default"); + + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, TestConfiguration.Password); + Assert.ThrowsAsync(() => client.AuthenticateAsClientAsync(credential, target.Substring(1))); + } + } + + public void Dispose() + { + try + { + _fixture.CheckAndInitializeKerberos(); + } + catch + { + } + } + } +} diff --git a/src/System.Net.Security/tests/FunctionalTests/NegotiateStreamStreamToStreamTest.cs b/src/System.Net.Security/tests/FunctionalTests/NegotiateStreamStreamToStreamTest.cs index ab5a1abb36a6..71054bb29756 100644 --- a/src/System.Net.Security/tests/FunctionalTests/NegotiateStreamStreamToStreamTest.cs +++ b/src/System.Net.Security/tests/FunctionalTests/NegotiateStreamStreamToStreamTest.cs @@ -261,13 +261,5 @@ public void NegotiateStream_StreamToStream_Successive_ClientWrite_Async_Success( Assert.True(_sampleMsg.SequenceEqual(recvBuf)); } } - - [ActiveIssue(6072, PlatformID.Linux | PlatformID.OSX)] - [Fact] - [PlatformSpecific(PlatformID.Linux | PlatformID.OSX)] - public void NegotiateStream_Ctor_Throws() - { - Assert.Throws(() => new NegotiateStream(new VirtualNetworkStream(null, isServer: false))); - } } } diff --git a/src/System.Net.Security/tests/FunctionalTests/Resources/Strings.resx b/src/System.Net.Security/tests/FunctionalTests/Resources/Strings.resx new file mode 100644 index 000000000000..ce944b34de88 --- /dev/null +++ b/src/System.Net.Security/tests/FunctionalTests/Resources/Strings.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Gss api operation failed with error : {0} ({1}). + + + GSSAPI operation failed with status: {0} (Minor status: {1}) + + + GSSAPI security context establishment failed with status: {0} (Minor status: {1}) + + + GSSAPI encryption or signing failed with status: {0} (Minor status: {1}) + + + GSSAPI decryption or signature verification failed with status: {0} (Minor status: {1}) + + + Insufficient buffer space. Required: {0} Actual: {1} + + diff --git a/src/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj b/src/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj index 6ca859fbade9..79d75ea728e3 100644 --- a/src/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj +++ b/src/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj @@ -1,4 +1,4 @@ - + Windows_Debug @@ -11,6 +11,10 @@ Library OSX + + true + + win/project.json win/project.lock.json @@ -77,6 +81,7 @@ Common\System\Threading\Tasks\TaskTimeoutExtensions.cs + @@ -98,6 +103,24 @@ Always + + + + + Common\Interop\Unix\Interop.Libraries.cs + + + Common\Interop\Unix\System.Net.Security.Native\Interop.GssApiException.cs + + + Common\Interop\Unix\System.Net.Security.Native\Interop.GssBuffer.cs + + + Common\Microsoft\Win32\SafeHandles\GssSafeHandles.cs + + + Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.cs + diff --git a/src/System.Net.Security/tests/FunctionalTests/TestConfiguration.cs b/src/System.Net.Security/tests/FunctionalTests/TestConfiguration.cs index a0e26c255aac..90ebbc860e1b 100644 --- a/src/System.Net.Security/tests/FunctionalTests/TestConfiguration.cs +++ b/src/System.Net.Security/tests/FunctionalTests/TestConfiguration.cs @@ -18,6 +18,13 @@ internal static class TestConfiguration private const string CertificatePassword = "testcertificate"; private const string TestDataFolder = "TestData"; + public const string Realm = "TEST.COREFX.NET"; + public const string KerberosUser = "krb_user"; + // TODO: using a hard-coded password, till issue #6329 is fixed + public const string Password = "password"; + public const string HostTarget = "TESTHOST/testfqdn.test.corefx.net"; + public const string HttpTarget = "TESTHTTP"; + public static X509Certificate2 GetServerCertificate() { X509Certificate2Collection certCollection = TestConfiguration.GetServerCertificateCollection(); diff --git a/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeNegotiateStream.cs b/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeNegotiateStream.cs new file mode 100644 index 000000000000..efb05cc84e27 --- /dev/null +++ b/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeNegotiateStream.cs @@ -0,0 +1,175 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security.Authentication; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.Net.Security.Tests +{ + internal class UnixGssFakeNegotiateStream : NegotiateStream + { + private static Action s_serverLoop = ServerLoop; + private static Action s_msgLoop = MessageLoop; + private readonly UnixGssFakeStreamFramer _framer; + private SafeGssContextHandle _context; + private volatile int _dataMsgCount; + + public UnixGssFakeNegotiateStream(Stream innerStream) : base(innerStream) + { + _framer = new UnixGssFakeStreamFramer(innerStream); + _dataMsgCount = 0; + } + + public override Task AuthenticateAsServerAsync() + { + return Task.Factory.StartNew(s_serverLoop, this, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + } + + public Task PollMessageAsync(int count) + { + _dataMsgCount = count; + return Task.Factory.StartNew(s_msgLoop, this, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + } + + public static void GetDefaultKerberosCredentials(string username, string password) + { + // Fetch a Kerberos TGT which gets saved in the default cache + SafeGssCredHandle.Create(username, password, string.Empty).Dispose(); + } + + private static void ServerLoop(object state) + { + UnixGssFakeNegotiateStream thisRef = (UnixGssFakeNegotiateStream)state; + var header = new byte[5]; + bool handshakeDone = false; + do + { + byte[] inBuf = thisRef._framer.ReadHandshakeFrame(); + byte[] outBuf = null; + try + { + handshakeDone = EstablishSecurityContext(ref thisRef._context, inBuf, out outBuf); + thisRef._framer.WriteHandshakeFrame(outBuf, 0, outBuf.Length); + } + catch (Interop.NetSecurityNative.GssApiException e) + { + thisRef._framer.WriteHandshakeFrame(e); + handshakeDone = true; + } + } + while (!handshakeDone); + } + + private static void MessageLoop(object state) + { + UnixGssFakeNegotiateStream thisRef = (UnixGssFakeNegotiateStream)state; + while (thisRef._dataMsgCount > 0) + { + byte[] inBuf = thisRef._framer.ReadDataFrame(); + byte[] unwrapped = UnwrapMessage(thisRef._context, inBuf); + byte[] outMsg = WrapMessage(thisRef._context, unwrapped); + thisRef._framer.WriteDataFrame(outMsg, 0, outMsg.Length); + thisRef._dataMsgCount--; + } + } + + private static bool EstablishSecurityContext( + ref SafeGssContextHandle context, + byte[] buffer, + out byte[] outputBuffer) + { + outputBuffer = null; + + // EstablishSecurityContext is called multiple times in a session. + // In each call, we need to pass the context handle from the previous call. + // For the first call, the context handle will be null. + if (context == null) + { + context = new SafeGssContextHandle(); + } + + Interop.NetSecurityNative.GssBuffer token = default(Interop.NetSecurityNative.GssBuffer); + Interop.NetSecurityNative.Status status; + + try + { + Interop.NetSecurityNative.Status minorStatus; + status = Interop.NetSecurityNative.AcceptSecContext(out minorStatus, + ref context, + buffer, + (buffer == null) ? 0 : buffer.Length, + ref token); + + if ((status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) && (status != Interop.NetSecurityNative.Status.GSS_S_CONTINUE_NEEDED)) + { + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + outputBuffer = token.ToByteArray(); + } + finally + { + token.Dispose(); + } + + return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE; + } + + private static byte[] UnwrapMessage(SafeGssContextHandle context, byte[] message) + { + Interop.NetSecurityNative.GssBuffer unwrapped = default(Interop.NetSecurityNative.GssBuffer); + Interop.NetSecurityNative.Status status; + + try + { + Interop.NetSecurityNative.Status minorStatus; + status = Interop.NetSecurityNative.Unwrap(out minorStatus, + context, + message, + 0, + message.Length, + ref unwrapped); + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + return unwrapped.ToByteArray(); + } + finally + { + unwrapped.Dispose(); + } + } + + private static byte[] WrapMessage(SafeGssContextHandle context, byte[] message) + { + Interop.NetSecurityNative.GssBuffer wrapped = default(Interop.NetSecurityNative.GssBuffer); + Interop.NetSecurityNative.Status status; + + try + { + Interop.NetSecurityNative.Status minorStatus; + status = Interop.NetSecurityNative.Wrap(out minorStatus, + context, + false, + message, + 0, + message.Length, + ref wrapped); + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + return wrapped.ToByteArray(); + } + finally + { + wrapped.Dispose(); + } + } + } +} diff --git a/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeStreamFramer.cs b/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeStreamFramer.cs new file mode 100644 index 000000000000..76ed04a2ffd6 --- /dev/null +++ b/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeStreamFramer.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.Net.Security.Tests +{ + internal class UnixGssFakeStreamFramer + { + public const byte HandshakeDoneId = 20; + public const byte HandshakeErrId = 21; + public const byte DefaultMajorV = 1; + public const byte DefaultMinorV = 0; + + private readonly Stream _innerStream; + private readonly byte[] _header = new byte[5]; + private static readonly byte[] ErrorBuffer = new byte[] { 0, 0, 0, 0, 0x80, 0x09, 0x03, 0x0C }; // return LOGON_DENIED + + public UnixGssFakeStreamFramer(Stream innerStream) + { + _innerStream = innerStream; + } + + public void WriteDataFrame(byte[] buffer, int offset, int count) + { + // data messages have the format of |pay-load-size|pay-load...| + // where, pay-load-size = size of the payload as unsigned-int in little endian format + // reference: https://msdn.microsoft.com/en-us/library/cc236740.aspx + + byte[] prefix = BitConverter.GetBytes(Convert.ToUInt32(count)); + if (!BitConverter.IsLittleEndian) + { + Array.Reverse(prefix); + } + + _innerStream.Write(prefix, 0, prefix.Length); + _innerStream.Write(buffer, offset, count); + } + + public void WriteHandshakeFrame(byte[] buffer, int offset, int count) + { + WriteFrameHeader(count, isError:false); + if (count > 0) + { + _innerStream.Write(buffer, offset, count); + } + } + + public void WriteHandshakeFrame(Interop.NetSecurityNative.GssApiException e) + { + WriteFrameHeader(ErrorBuffer.Length, isError:true); + _innerStream.Write(ErrorBuffer, 0, ErrorBuffer.Length); + } + + public byte[] ReadHandshakeFrame() + { + // A handshake header described at https://msdn.microsoft.com/en-us/library/cc236739.aspx + // consists of 5 bytes: + // first byte is a message id (one of [HandshakeDoneId, HandshakeErrId, HandshakeInProgress]) + // second byte is Major version of protocol (0x01) + // third byte is Minor version of protocol (0) + // fourth byte is the high order byte of the payload size (expressed as an unsigned number - ushort) + // fifth byte is the low order byte of the payload size (expressed as unsigned number - ushort) + + _innerStream.Read(_header, 0, _header.Length); + byte[] inBuf = new byte[(_header[3] << 8) + _header[4]]; + _innerStream.Read(inBuf, 0, inBuf.Length); + return inBuf; + } + + public byte[] ReadDataFrame() + { + // data messages have the format of |pay-load-size|pay-load...| + // where, pay-load-size = size of the payload as unsigned-int in little endian format + // reference: https://msdn.microsoft.com/en-us/library/cc236740.aspx + + byte[] lenBytes = new byte[4]; + _innerStream.Read(lenBytes, 0, lenBytes.Length); + if (!BitConverter.IsLittleEndian) + { + Array.Reverse(lenBytes); + } + + int length = Convert.ToInt32(BitConverter.ToUInt32(lenBytes, startIndex: 0)); + byte[] data = new byte[length]; + _innerStream.Read(data, 0, length); + return data; + } + + private void WriteFrameHeader(int count, bool isError) + { + // A handshake header described at https://msdn.microsoft.com/en-us/library/cc236739.aspx + // consists of 5 bytes: + // first byte is a message id (one of [HandshakeDoneId, HandshakeErrId, HandshakeInProgress]) + // second byte is Major version of protocol (0x01) + // third byte is Minor version of protocol (0) + // fourth byte is the high order byte of the payload size (expressed as unsigned number - ushort) + // fifth byte is the low order byte of the payload size (expressed as unsigned number - ushort) + + _header[0] = isError ? HandshakeErrId : HandshakeDoneId; + _header[1] = DefaultMajorV; + _header[2] = DefaultMinorV; + _header[3] = (byte)((count >> 8) & 0xff); + _header[4] = (byte)(count & 0xff); + _innerStream.Write(_header, 0, _header.Length); + } + } +} diff --git a/src/System.Net.Security/tests/FunctionalTests/unix/project.json b/src/System.Net.Security/tests/FunctionalTests/unix/project.json index d4bc952495d1..341bfc77da35 100644 --- a/src/System.Net.Security/tests/FunctionalTests/unix/project.json +++ b/src/System.Net.Security/tests/FunctionalTests/unix/project.json @@ -6,6 +6,7 @@ "System.Net.TestData": "1.0.0-prerelease", "System.Security.Cryptography.X509Certificates": "4.0.0-rc3-23808", "System.Security.Principal": "4.0.0", + "System.Diagnostics.Process": "4.1.0-rc3-23808", "xunit": "2.1.0", "xunit.netcore.extensions": "1.0.0-prerelease-00123" },