diff --git a/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeKdcServer.cs b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeKdcServer.cs new file mode 100644 index 00000000000000..cc4414bb69724b --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeKdcServer.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Kerberos.NET.Server; + +namespace System.Net.Security.Kerberos; + +class FakeKdcServer +{ + private readonly KdcServer _kdcServer; + private readonly TcpListener _tcpListener; + private CancellationTokenSource? _cancellationTokenSource; + private bool _running; + private readonly object _runningLock; + + public FakeKdcServer(KdcServerOptions serverOptions) + { + _kdcServer = new KdcServer(serverOptions); + _tcpListener = new TcpListener(System.Net.IPAddress.Loopback, 0); + _runningLock = new object(); + } + + public Task Start() + { + _cancellationTokenSource = new CancellationTokenSource(); + _running = true; + _tcpListener.Start(); + + var cancellationToken = _cancellationTokenSource.Token; + Task.Run(async () => { + try + { + byte[] sizeBuffer = new byte[4]; + do + { + using var socket = await _tcpListener.AcceptSocketAsync(cancellationToken); + using var socketStream = new NetworkStream(socket); + + await socketStream.ReadExactlyAsync(sizeBuffer, cancellationToken); + var messageSize = BinaryPrimitives.ReadInt32BigEndian(sizeBuffer); + var requestRented = ArrayPool.Shared.Rent(messageSize); + var request = requestRented.AsMemory(0, messageSize); + await socketStream.ReadExactlyAsync(request); + var response = await _kdcServer.ProcessMessage(request); + ArrayPool.Shared.Return(requestRented); + var responseLength = response.Length + 4; + var responseRented = ArrayPool.Shared.Rent(responseLength); + BinaryPrimitives.WriteInt32BigEndian(responseRented.AsSpan(0, 4), responseLength); + response.CopyTo(responseRented.AsMemory(4, responseLength)); + await socketStream.WriteAsync(responseRented.AsMemory(0, responseLength + 4), cancellationToken); + ArrayPool.Shared.Return(responseRented); + } + while (!cancellationToken.IsCancellationRequested); + } + finally + { + lock (_runningLock) + { + _running = false; + Monitor.Pulse(_runningLock); + } + } + }); + return Task.FromResult((IPEndPoint)_tcpListener.LocalEndpoint); + } + + public void Stop() + { + if (_running) + { + _cancellationTokenSource?.Cancel(); + lock (_runningLock) + { + while (_running) + { + Monitor.Wait(_runningLock); + } + } + _tcpListener.Stop(); + } + } +} diff --git a/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeKerberosPrincipal.cs b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeKerberosPrincipal.cs new file mode 100644 index 00000000000000..0fe771efadda53 --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeKerberosPrincipal.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using Kerberos.NET.Crypto; +using Kerberos.NET.Entities; +using Kerberos.NET.Server; + +namespace System.Net.Security.Kerberos; + +class FakeKerberosPrincipal : IKerberosPrincipal +{ + private readonly byte[] _password; + + public FakeKerberosPrincipal(PrincipalType type, string principalName, string realm, byte[] password) + { + this.Type = type; + this.PrincipalName = principalName; + this.Realm = realm; + this.Expires = DateTimeOffset.UtcNow.AddMonths(9999); + this._password = password; + } + + public SupportedEncryptionTypes SupportedEncryptionTypes { get; set; } + = SupportedEncryptionTypes.Aes128CtsHmacSha196 | + SupportedEncryptionTypes.Aes256CtsHmacSha196 | + SupportedEncryptionTypes.Aes128CtsHmacSha256 | + SupportedEncryptionTypes.Aes256CtsHmacSha384 | + SupportedEncryptionTypes.Rc4Hmac | + SupportedEncryptionTypes.DesCbcCrc | + SupportedEncryptionTypes.DesCbcMd5; + + public IEnumerable SupportedPreAuthenticationTypes { get; set; } = new[] + { + PaDataType.PA_ENC_TIMESTAMP, + PaDataType.PA_PK_AS_REQ + }; + + public PrincipalType Type { get; private set; } + + public string PrincipalName { get; private set; } + + public string Realm { get; private set; } + + public DateTimeOffset? Expires { get; set; } + + public PrivilegedAttributeCertificate? GeneratePac() => null; + + private static readonly ConcurrentDictionary KeyCache = new(); + + public KerberosKey RetrieveLongTermCredential() + { + return this.RetrieveLongTermCredential(EncryptionType.AES256_CTS_HMAC_SHA1_96); + } + + public KerberosKey RetrieveLongTermCredential(EncryptionType etype) + { + return KeyCache.GetOrAdd(etype + this.PrincipalName, pn => + { + return new KerberosKey( + password: this._password, + principal: new PrincipalName(PrincipalNameType.NT_PRINCIPAL, Realm, new[] { this.PrincipalName }), + etype: etype, + saltType: SaltType.ActiveDirectoryUser); + }); + } + + public void Validate(X509Certificate2Collection certificates) + { + } +} diff --git a/src/libraries/Common/tests/System/Net/Security/Kerberos/FakePrincipalService.cs b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakePrincipalService.cs new file mode 100644 index 00000000000000..2cdc928fdbd6f3 --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakePrincipalService.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Kerberos.NET.Crypto; +using Kerberos.NET.Entities; +using Kerberos.NET.Server; + +namespace System.Net.Security.Kerberos; + +class FakePrincipalService : IPrincipalService +{ + private readonly string _realm; + private readonly Dictionary _principals; + + public FakePrincipalService(string realm) + { + _realm = realm; + _principals = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + } + + public void Add(string name, IKerberosPrincipal principal) + { + _principals.Add(name, principal); + } + + public Task FindAsync(KrbPrincipalName principalName, string? realm = null) + { + return Task.FromResult(Find(principalName, realm)); + } + + public IKerberosPrincipal? Find(KrbPrincipalName principalName, string? realm = null) + { + if (_principals.TryGetValue(principalName.FullyQualifiedName, out var principal)) + { + return principal; + } + + return null; + } + + public X509Certificate2 RetrieveKdcCertificate() + { + throw new NotImplementedException(); + } + + private static readonly Dictionary KeyCache = new(); + + public IExchangeKey? RetrieveKeyCache(KeyAgreementAlgorithm algorithm) + { + if (KeyCache.TryGetValue(algorithm, out IExchangeKey? key)) + { + if (key.CacheExpiry < DateTimeOffset.UtcNow) + { + KeyCache.Remove(algorithm); + } + else + { + return key; + } + } + + return null; + } + + public IExchangeKey CacheKey(IExchangeKey key) + { + key.CacheExpiry = DateTimeOffset.UtcNow.AddMinutes(60); + + KeyCache[key.Algorithm] = key; + + return key; + } +} diff --git a/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeRealmReferral.cs b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeRealmReferral.cs new file mode 100644 index 00000000000000..9c8c4fa3898ac7 --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeRealmReferral.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Kerberos.NET.Entities; +using Kerberos.NET.Server; + +namespace System.Net.Security.Kerberos; + +class FakeRealmReferral : IRealmReferral +{ + private readonly KrbKdcReqBody _body; + + public FakeRealmReferral(KrbKdcReqBody body) + { + _body = body; + } + + public IKerberosPrincipal Refer() + { + var fqn = _body.SName.FullyQualifiedName; + var predictedRealm = fqn[(fqn.IndexOf('.') + 1)..]; + + var krbName = KrbPrincipalName.FromString($"krbtgt/{predictedRealm}"); + + return new FakeKerberosPrincipal(PrincipalType.Service, krbName.FullyQualifiedName, predictedRealm, new byte[16]); + } +} diff --git a/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeRealmService.cs b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeRealmService.cs new file mode 100644 index 00000000000000..b590fb87b4cabd --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeRealmService.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Kerberos.NET.Configuration; +using Kerberos.NET.Server; + +namespace System.Net.Security.Kerberos; + +class FakeRealmService : IRealmService +{ + private readonly IPrincipalService _principalService; + private readonly KerberosCompatibilityFlags _compatibilityFlags; + + public FakeRealmService(string realm, Krb5Config config, IPrincipalService principalService, KerberosCompatibilityFlags compatibilityFlags = KerberosCompatibilityFlags.None) + { + Name = realm; + Configuration = config; + _principalService = principalService; + _compatibilityFlags = compatibilityFlags; + } + + public IRealmSettings Settings => new FakeRealmSettings(_compatibilityFlags); + + public IPrincipalService Principals => _principalService; + + public string Name { get; private set; } + + public DateTimeOffset Now() => DateTimeOffset.UtcNow; + + public ITrustedRealmService TrustedRealms => new FakeTrustedRealms(this.Name); + + public Krb5Config Configuration { get; private set; } +} \ No newline at end of file diff --git a/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeRealmSettings.cs b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeRealmSettings.cs new file mode 100644 index 00000000000000..8e14ea8d456987 --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeRealmSettings.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Kerberos.NET.Server; + +namespace System.Net.Security.Kerberos; + +class FakeRealmSettings : IRealmSettings +{ + private readonly KerberosCompatibilityFlags _compatibilityFlags; + + public FakeRealmSettings(KerberosCompatibilityFlags compatibilityFlags) + { + _compatibilityFlags = compatibilityFlags; + } + + public TimeSpan MaximumSkew => TimeSpan.FromMinutes(5); + + public TimeSpan SessionLifetime => TimeSpan.FromHours(10); + + public TimeSpan MaximumRenewalWindow => TimeSpan.FromDays(7); + + public KerberosCompatibilityFlags Compatibility => _compatibilityFlags; +} diff --git a/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeTrustedRealms.cs b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeTrustedRealms.cs new file mode 100644 index 00000000000000..11fc0208880d22 --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Security/Kerberos/FakeTrustedRealms.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Kerberos.NET.Entities; +using Kerberos.NET.Server; + +namespace System.Net.Security.Kerberos; + +class FakeTrustedRealms : ITrustedRealmService +{ + private readonly string _currentRealm; + + public FakeTrustedRealms(string name) + { + _currentRealm = name; + } + + public IRealmReferral? ProposeTransit(KrbTgsReq tgsReq, PreAuthenticationContext context) + { + if (!tgsReq.Body.SName.FullyQualifiedName.EndsWith(_currentRealm, StringComparison.InvariantCultureIgnoreCase) && + !tgsReq.Body.SName.FullyQualifiedName.Contains("not.found")) + { + return new FakeRealmReferral(tgsReq.Body); + } + + return null; + } +} diff --git a/src/libraries/Common/tests/System/Net/Security/Kerberos/KerberosExecutor.cs b/src/libraries/Common/tests/System/Net/Security/Kerberos/KerberosExecutor.cs new file mode 100644 index 00000000000000..b4b7f097dba5f7 --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Security/Kerberos/KerberosExecutor.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.RemoteExecutor; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Kerberos.NET.Configuration; +using Kerberos.NET.Crypto; +using Kerberos.NET.Entities; +using Kerberos.NET.Server; +using Kerberos.NET.Logging; +using Xunit.Abstractions; + +namespace System.Net.Security.Kerberos; + +public class KerberosExecutor : IDisposable +{ + private readonly ListenerOptions _options; + private readonly string _realm; + private readonly FakePrincipalService _principalService; + private readonly FakeKdcServer _kdcListener; + private RemoteInvokeHandle? _invokeHandle; + private string? _krb5Path; + private string? _keytabPath; + private readonly List _servicePrincipals; + + public static bool IsSupported { get; } = OperatingSystem.IsLinux() || OperatingSystem.IsMacOS(); + + public const string DefaultAdminPassword = "PLACEHOLDERadmin."; + + public const string DefaultUserPassword = "PLACEHOLDERcorrect20"; + + public KerberosExecutor(ITestOutputHelper testOutputHelper, string realm) + { + var krb5Config = Krb5Config.Default(); + krb5Config.KdcDefaults.RegisterDefaultPkInitPreAuthHandler = false; + + var logger = new KerberosDelegateLogger( + (level, categoryName, eventId, scopeState, logState, exception, log) => + testOutputHelper.WriteLine($"[{level}] [{categoryName}] {log}") + ); + + _principalService = new FakePrincipalService(realm); + + byte[] krbtgtPassword = new byte[16]; + //RandomNumberGenerator.Fill(krbtgtPassword); + + var krbtgt = new FakeKerberosPrincipal(PrincipalType.Service, "krbtgt", realm, krbtgtPassword); + _principalService.Add("krbtgt", krbtgt); + _principalService.Add($"krbtgt/{realm}", krbtgt); + + _options = new ListenerOptions + { + Configuration = krb5Config, + DefaultRealm = realm, + RealmLocator = realm => new FakeRealmService(realm, krb5Config, _principalService), + Log = logger, + IsDebug = true, + }; + + _kdcListener = new FakeKdcServer(_options); + _realm = realm; + _servicePrincipals = new List(); + } + + public void Dispose() + { + _invokeHandle?.Dispose(); + _kdcListener.Stop(); + File.Delete(_krb5Path); + File.Delete(_keytabPath); + } + + public void AddService(string name, string password = DefaultAdminPassword) + { + var principal = new FakeKerberosPrincipal(PrincipalType.Service, name, _realm, Encoding.Unicode.GetBytes(password)); + _principalService.Add(name, principal); + _servicePrincipals.Add(principal); + } + + public void AddUser(string name, string password = DefaultUserPassword) + { + var principal = new FakeKerberosPrincipal(PrincipalType.User, name, _realm, Encoding.Unicode.GetBytes(password)); + _principalService.Add(name, principal); + _principalService.Add($"{name}@{_realm}", principal); + } + + public async Task Invoke(Action method) + { + await PrepareInvoke(); + _invokeHandle = RemoteExecutor.Invoke(method); + } + + public async Task Invoke(Func method) + { + await PrepareInvoke(); + _invokeHandle = RemoteExecutor.Invoke(method); + } + + private async Task PrepareInvoke() + { + // Start the KDC server + var endpoint = await _kdcListener.Start(); + + // Generate krb5.conf + _krb5Path = Path.GetTempFileName(); + File.WriteAllText(_krb5Path, + OperatingSystem.IsLinux() ? + $"[realms]\n{_options.DefaultRealm} = {{\n master_kdc = {endpoint}\n kdc = {endpoint}\n}}\n" : + $"[realms]\n{_options.DefaultRealm} = {{\n kdc = tcp/{endpoint}\n}}\n"); + + // Generate keytab file + _keytabPath = Path.GetTempFileName(); + var keyTable = new KeyTable(); + + var etypes = _options.Configuration.Defaults.DefaultTgsEncTypes; + //byte[] passwordBytes = FakeKerberosPrincipal.FakePassword; + + foreach (var servicePrincipal in _servicePrincipals) + { + foreach (var etype in etypes.Where(CryptoService.SupportsEType)) + { + var kerbKey = servicePrincipal.RetrieveLongTermCredential(etype); + keyTable.Entries.Add(new KeyEntry(kerbKey)); + } + } + + using (var fs = new FileStream(_keytabPath, FileMode.Create)) + using (var writer = new BinaryWriter(fs)) + { + keyTable.Write(writer); + writer.Flush(); + } + + // Set environment variables for GSSAPI + Environment.SetEnvironmentVariable("KRB5_CONFIG", _krb5Path); + Environment.SetEnvironmentVariable("KRB5_KTNAME", _keytabPath); + } +} \ No newline at end of file diff --git a/src/libraries/Common/tests/System/Net/Security/Kerberos/README.md b/src/libraries/Common/tests/System/Net/Security/Kerberos/README.md new file mode 100644 index 00000000000000..df8bfc792e15a4 --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Security/Kerberos/README.md @@ -0,0 +1,53 @@ +# Kerberos Testing Environment + +## Introduction + +In a typical enterprise setting, there is a domain controller that handles authentication requests and clients connecting to it. The clients here are referring both to the clients in a traditional sense (eg. `HttpClient`) and the services that are running on the server-side (eg. `HttpListener`). + +Replicating that environment for unit testing is non-trivial since it usually requires multiple machines and adjusting system configuration. Alternatively, it could be set up using containers running on a single machine with a preprepared configuration. Unfortunately, using containers restricts the possibility of testing various operating systems, or at least makes it non-trivial. + +To make the setup more approachable, this directory contains an implementation of `KerberosExecutor` class that sets up a virtual environment with Kerberos Domain Controller (KDC) running inside the unit test. The system configuration is then locally redirected for the test itself through environment variables to connect to this KDC instead of a system one. All the tests are run within a `RemoteExecutor` environment. The KDC is powered by the [Kerberos.NET](https://github.com/dotnet/Kerberos.NET) library. + +## Usage + +Since the environment currently works only on Linux and macOS platforms the test class or test method needs to be decorated to only run when `KerberosExecutor` is supported: + +```csharp +[ConditionalClass(typeof(KerberosExecutor), nameof(KerberosExecutor.IsSupported))] +public class MyKerberosTest +``` + +The xUnit logging through `ITestOutputHelper` must be set up for the test class: + +```csharp +private readonly ITestOutputHelper _testOutputHelper; + +public MyKerberosTest(ITestOutputHelper testOutputHelper) +{ + _testOutputHelper = testOutputHelper; +} +``` + +Each test then uses the `KerberosExecutor` class to set up the virtual environment and the test code to run inside the virtual environment: + +```csharp +[Fact] +public async Task Loopback_Success() +{ + using var kerberosExecutor = new KerberosExecutor(_testOutputHelper, "LINUX.CONTOSO.COM"); + + kerberosExecutor.AddService("HTTP/linux.contoso.com"); + kerberosExecutor.AddUser("user"); + + await kerberosExecutor.Invoke(() => + { + // Test code + } +} +``` + +The test itself can add its own users and services. Each service must be specified using full service principal name (SPN). In the example above the default password is used for the user, so the test would use `new NetworkCredential("user", KerberosExecutor.DefaultUserPassword, "LINUX.CONTOSO.COM")` to construct credentials for use in `HttpClient` or similar scenario. + +## Logging + +For failed unit test the verbose output of the KDC server is logged into the test output. If the information is insufficient it is possible to get a trace from the native libraries by setting the `KRB5_TRACE="/dev/stdout"` environment variable. \ No newline at end of file diff --git a/src/libraries/Common/tests/System/Net/Security/Kerberos/System.Net.Security.Kerberos.Shared.projitems b/src/libraries/Common/tests/System/Net/Security/Kerberos/System.Net.Security.Kerberos.Shared.projitems new file mode 100644 index 00000000000000..6371a6a6b6fb70 --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Security/Kerberos/System.Net.Security.Kerberos.Shared.projitems @@ -0,0 +1,41 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 4aaf81e6-6cdc-44fa-adfd-d7b47b9c998f + + + + true + + + + + CommonTest\System\Net\Security\Kerberos\FakeKdcServer.cs + + + CommonTest\System\Net\Security\Kerberos\FakeKerberosPrincipal.cs + + + CommonTest\System\Net\Security\Kerberos\FakePrincipalService.cs + + + CommonTest\System\Net\Security\Kerberos\FakeRealmReferral.cs + + + CommonTest\System\Net\Security\Kerberos\FakeRealmService.cs + + + CommonTest\System\Net\Security\Kerberos\FakeRealmSettings.cs + + + CommonTest\System\Net\Security\Kerberos\FakeTrustedRealms.cs + + + CommonTest\System\Net\Security\Kerberos\KerberosExecutor.cs + + + + + + diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/NegotiateAuthenticationKerberosTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/NegotiateAuthenticationKerberosTest.cs new file mode 100644 index 00000000000000..3bb9a1ad5cc2f8 --- /dev/null +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/NegotiateAuthenticationKerberosTest.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using System.Net.Security.Kerberos; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Security.Tests +{ + [ConditionalClass(typeof(KerberosExecutor), nameof(KerberosExecutor.IsSupported))] + public class NegotiateAuthenticationKerberosTest + { + private readonly ITestOutputHelper _testOutputHelper; + + public NegotiateAuthenticationKerberosTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + [Fact] + public async Task Loopback_Success() + { + using var kerberosExecutor = new KerberosExecutor(_testOutputHelper, "LINUX.CONTOSO.COM"); + + kerberosExecutor.AddService("HTTP/linux.contoso.com"); + kerberosExecutor.AddUser("user"); + + await kerberosExecutor.Invoke(() => + { + // Do a loopback authentication + NegotiateAuthenticationClientOptions clientOptions = new() + { + Credential = new NetworkCredential("user", KerberosExecutor.DefaultUserPassword, "LINUX.CONTOSO.COM"), + TargetName = $"HTTP/linux.contoso.com" + }; + NegotiateAuthenticationServerOptions serverOptions = new() { }; + NegotiateAuthentication clientNegotiateAuthentication = new(clientOptions); + NegotiateAuthentication serverNegotiateAuthentication = new(serverOptions); + + byte[]? serverBlob = null; + byte[]? clientBlob = null; + bool shouldContinue = true; + do + { + clientBlob = clientNegotiateAuthentication.GetOutgoingBlob(serverBlob, out NegotiateAuthenticationStatusCode statusCode); + shouldContinue = statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded; + Assert.True(statusCode <= NegotiateAuthenticationStatusCode.ContinueNeeded, $"Client authentication failed with {statusCode}"); + if (clientBlob != null) + { + serverBlob = serverNegotiateAuthentication.GetOutgoingBlob(clientBlob, out statusCode); + Assert.True(statusCode <= NegotiateAuthenticationStatusCode.ContinueNeeded, $"Server authentication failed with {statusCode}"); + } + } + while (serverBlob != null && shouldContinue); + + Assert.Equal("Kerberos", clientNegotiateAuthentication.Package); + Assert.Equal("Kerberos", serverNegotiateAuthentication.Package); + Assert.True(clientNegotiateAuthentication.IsAuthenticated); + Assert.True(serverNegotiateAuthentication.IsAuthenticated); + }); + } + + [Fact] + public async void Invalid_Token() + { + using var kerberosExecutor = new KerberosExecutor(_testOutputHelper, "LINUX.CONTOSO.COM"); + // Force a non-empty keytab to make macOS happy + kerberosExecutor.AddService("HTTP/linux.contoso.com"); + await kerberosExecutor.Invoke(() => + { + NegotiateAuthentication ntAuth = new NegotiateAuthentication(new NegotiateAuthenticationServerOptions { }); + // Ask for NegHints + byte[] blob = ntAuth.GetOutgoingBlob((ReadOnlySpan)default, out NegotiateAuthenticationStatusCode statusCode); + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, statusCode); + Assert.NotNull(blob); + // Send garbage token + blob = ntAuth.GetOutgoingBlob(new byte[3], out statusCode); + Assert.True(statusCode >= NegotiateAuthenticationStatusCode.GenericFailure); + Assert.Null(blob); + }); + } + } +} diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj index 742c49799eaf9b..2c5f9d30fdcfb4 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj @@ -9,6 +9,7 @@ + @@ -28,6 +29,8 @@ + +