-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Add Kerberos loopback test #71824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Kerberos loopback test #71824
Changes from all commits
4e2dfa4
12b65a1
f95e6e2
ff0c20c
e928da5
c6619e1
027f967
67999aa
98c4f75
fa99451
b2e8daf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| _runningLock = new object(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need new object?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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