Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: IPAddress.Loopback? We may eventually figure out how to do dual mode but this is ok for now IMHO

_runningLock = new object();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need new object?
object _runningLock => TcpListener ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we don't. It's a habit not to reuse other objects for locks because it's prone to misuse but I can change it.

}

public Task<IPEndPoint> 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<byte>.Shared.Rent(messageSize);
var request = requestRented.AsMemory(0, messageSize);
await socketStream.ReadExactlyAsync(request);
var response = await _kdcServer.ProcessMessage(request);
ArrayPool<byte>.Shared.Return(requestRented);
var responseLength = response.Length + 4;
var responseRented = ArrayPool<byte>.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<byte>.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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would change the name to decrease chance we get flagged by security scans.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC-wise it's called "long term secret"


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<PaDataType> 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<string, KerberosKey> 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)
{
}
}
Original file line number Diff line number Diff line change
@@ -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<string, IKerberosPrincipal> _principals;

public FakePrincipalService(string realm)
{
_realm = realm;
_principals = new Dictionary<string, IKerberosPrincipal>(StringComparer.InvariantCultureIgnoreCase);
}

public void Add(string name, IKerberosPrincipal principal)
{
_principals.Add(name, principal);
}

public Task<IKerberosPrincipal?> 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<KeyAgreementAlgorithm, IExchangeKey> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we initialize the password to something? (may not matter but looks odd to me)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine as long as it's consistently x00 everywhere and is never used for anything in production.

}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading