From f610c234ac1f6763ac38e151b6aa04a0dc5c3ff1 Mon Sep 17 00:00:00 2001 From: "Theo Dumitrescu (from Dev Box)" Date: Fri, 20 Feb 2026 02:37:51 +0000 Subject: [PATCH 1/3] Add GSS channel binding support per RFC 4121 section 4.1.1.2 for AP-REQ --- Kerberos.NET/Crypto/DecryptedKrbApReq.cs | 44 ++ .../Entities/GssApi/GssChannelBindings.cs | 68 ++++ Kerberos.NET/Entities/Krb/DelegationInfo.cs | 5 + Kerberos.NET/Entities/RequestServiceTicket.cs | 5 + Kerberos.NET/KerberosValidator.cs | 6 + Kerberos.NET/ValidationAction.cs | 7 +- .../KrbApReq/ChannelBindingTests.cs | 379 ++++++++++++++++++ 7 files changed, 513 insertions(+), 1 deletion(-) create mode 100644 Kerberos.NET/Entities/GssApi/GssChannelBindings.cs create mode 100644 Tests/Tests.Kerberos.NET/KrbApReq/ChannelBindingTests.cs diff --git a/Kerberos.NET/Crypto/DecryptedKrbApReq.cs b/Kerberos.NET/Crypto/DecryptedKrbApReq.cs index 45682a71..c199789d 100644 --- a/Kerberos.NET/Crypto/DecryptedKrbApReq.cs +++ b/Kerberos.NET/Crypto/DecryptedKrbApReq.cs @@ -36,6 +36,19 @@ public DecryptedKrbApReq(KrbApReq token, MessageType incomingMessageType = Messa public KrbEncKrbCredPart DelegationTicket { get; private set; } + /// + /// The channel binding hash (Bnd field) extracted from the authenticator checksum, + /// as described in RFC 4121 section 4.1.1.2. This is a 16-byte MD5 hash of the + /// structure. Will be null or empty if no channel + /// bindings were supplied by the initiator. + /// + public ReadOnlyMemory ChannelBindingHash { get; private set; } + + /// + /// Expected channel bindings to validate against when is enabled. + /// + public GssChannelBindings ExpectedChannelBindings { get; set; } + public KerberosKey SessionKey { get; private set; } private readonly KrbApReq token; @@ -161,6 +174,8 @@ private KrbEncKrbCredPart TryExtractDelegationTicket(KrbChecksum checksum) var delegationInfo = checksum.DecodeDelegation(); + this.ChannelBindingHash = delegationInfo.ChannelBinding; + var delegation = delegationInfo?.DelegationTicket; if (delegation == null) @@ -212,6 +227,35 @@ public override void Validate(ValidationActions validation) { this.ValidateTicketRenewal(this.Ticket.RenewTill, now, this.Skew); } + + if (validation.HasFlag(ValidationActions.ChannelBinding)) + { + this.ValidateChannelBinding(); + } + } + + protected virtual void ValidateChannelBinding() + { + if (this.ExpectedChannelBindings == null) + { + return; + } + + var expectedHash = this.ExpectedChannelBindings.ComputeBindingHash(); + + if (this.ChannelBindingHash.Length == 0) + { + throw new KerberosValidationException( + "Channel Bindings are required by the acceptor but were not supplied by the initiator." + ); + } + + if (!KerberosCryptoTransformer.AreEqualSlow(expectedHash.Span, this.ChannelBindingHash.Span)) + { + throw new KerberosValidationException( + "The Channel Bindings hash from the initiator does not match the expected channel bindings." + ); + } } public override string ToString() diff --git a/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs b/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs new file mode 100644 index 00000000..39ec0a48 --- /dev/null +++ b/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs @@ -0,0 +1,68 @@ +// ----------------------------------------------------------------------- +// Licensed to The .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// ----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace Kerberos.NET.Entities +{ + /// + /// Represents gss_channel_bindings_struct per RFC 4121 section 4.1.1.2. + /// + public class GssChannelBindings + { + public int InitiatorAddrType { get; set; } + + public ReadOnlyMemory InitiatorAddress { get; set; } + + public int AcceptorAddrType { get; set; } + + public ReadOnlyMemory AcceptorAddress { get; set; } + + /// + /// Protocol-specific channel binding data + /// e.g. tls-server-end-point or tls-unique as per RFC 5929 + /// + public ReadOnlyMemory ApplicationData { get; set; } + + /// + /// Computes the 16-byte MD5 binding hash (Bnd field) per RFC 4121 section 4.1.1.2. + /// + public ReadOnlyMemory ComputeBindingHash() + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + + writer.Write(this.InitiatorAddrType); + writer.Write(this.InitiatorAddress.Length); + + if (this.InitiatorAddress.Length > 0) + { + writer.Write(this.InitiatorAddress.ToArray()); + } + + writer.Write(this.AcceptorAddrType); + writer.Write(this.AcceptorAddress.Length); + + if (this.AcceptorAddress.Length > 0) + { + writer.Write(this.AcceptorAddress.ToArray()); + } + + writer.Write(this.ApplicationData.Length); + + if (this.ApplicationData.Length > 0) + { + writer.Write(this.ApplicationData.ToArray()); + } + + var data = stream.ToArray(); + + using var md5 = MD5.Create(); + return md5.ComputeHash(data); + } + } +} diff --git a/Kerberos.NET/Entities/Krb/DelegationInfo.cs b/Kerberos.NET/Entities/Krb/DelegationInfo.cs index 4c167ca7..e7f91e6e 100644 --- a/Kerberos.NET/Entities/Krb/DelegationInfo.cs +++ b/Kerberos.NET/Entities/Krb/DelegationInfo.cs @@ -42,6 +42,11 @@ public DelegationInfo() public DelegationInfo(RequestServiceTicket rst) { this.Flags = rst.GssContextFlags; + + if (rst.ChannelBindings != null) + { + this.ChannelBinding = rst.ChannelBindings.ComputeBindingHash(); + } } public ReadOnlyMemory Encode() diff --git a/Kerberos.NET/Entities/RequestServiceTicket.cs b/Kerberos.NET/Entities/RequestServiceTicket.cs index 009630f5..d5e78167 100644 --- a/Kerberos.NET/Entities/RequestServiceTicket.cs +++ b/Kerberos.NET/Entities/RequestServiceTicket.cs @@ -65,6 +65,11 @@ public struct RequestServiceTicket : IEquatable /// public GssContextEstablishmentFlag GssContextFlags { get; set; } + /// + /// Optional GSS channel bindings to include in the authenticator checksum. + /// + public GssChannelBindings ChannelBindings { get; set; } + /// /// Includes additional configuration details for the request. /// diff --git a/Kerberos.NET/KerberosValidator.cs b/Kerberos.NET/KerberosValidator.cs index 1e8ba9d3..f9787318 100644 --- a/Kerberos.NET/KerberosValidator.cs +++ b/Kerberos.NET/KerberosValidator.cs @@ -46,6 +46,11 @@ public KerberosValidator(KeyTable keytab, ILoggerFactory logger = null, ITicketR public ValidationActions ValidateAfterDecrypt { get; set; } + /// + /// Expected channel bindings to validate during decryption. + /// + public GssChannelBindings ExpectedChannelBindings { get; set; } + private Func nowFunc; public Func Now @@ -83,6 +88,7 @@ public async Task Validate(ReadOnlyMemory requestBytes) this.logger.LogTrace("Kerberos request decrypted {SName}", decryptedToken.SName.FullyQualifiedName); decryptedToken.Now = this.Now; + decryptedToken.ExpectedChannelBindings = this.ExpectedChannelBindings; if (this.ValidateAfterDecrypt > 0) { diff --git a/Kerberos.NET/ValidationAction.cs b/Kerberos.NET/ValidationAction.cs index 8bb803e5..542d2dbd 100644 --- a/Kerberos.NET/ValidationAction.cs +++ b/Kerberos.NET/ValidationAction.cs @@ -70,9 +70,14 @@ public enum ValidationActions /// SequenceNumberGreaterThan = 1 << 9, + /// + /// Validates channel bindings in the authenticator checksum. + /// + ChannelBinding = 1 << 10, + /// /// Indicates all validation actions must be invoked. /// - All = ClientPrincipalIdentifier | Realm | TokenWindow | StartTime | EndTime | Replay | Pac | RenewTill + All = ClientPrincipalIdentifier | Realm | TokenWindow | StartTime | EndTime | Replay | Pac | RenewTill | ChannelBinding } } diff --git a/Tests/Tests.Kerberos.NET/KrbApReq/ChannelBindingTests.cs b/Tests/Tests.Kerberos.NET/KrbApReq/ChannelBindingTests.cs new file mode 100644 index 00000000..97f09b6b --- /dev/null +++ b/Tests/Tests.Kerberos.NET/KrbApReq/ChannelBindingTests.cs @@ -0,0 +1,379 @@ +// ----------------------------------------------------------------------- +// Licensed to The .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// ----------------------------------------------------------------------- + +using System; +using System.Linq; +using System.Security.Cryptography; +using Kerberos.NET; +using Kerberos.NET.Crypto; +using Kerberos.NET.Entities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Tests.Kerberos.NET +{ + [TestClass] + public class ChannelBindingTests : BaseTest + { + private static readonly byte[] SampleTlsBinding = new byte[] + { + 0x74, 0x6C, 0x73, 0x2D, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x2D, 0x65, 0x6E, 0x64, 0x2D, 0x70, + 0x6F, 0x69, 0x6E, 0x74, 0x3A, 0xAA, 0xBB, 0xCC, + 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44 + }; + + // -- GssChannelBindings hash computation tests -- + + [TestMethod] + public void ChannelBindings_ComputeHash_ReturnsFixedLength() + { + var bindings = new GssChannelBindings + { + ApplicationData = SampleTlsBinding + }; + + var hash = bindings.ComputeBindingHash(); + + Assert.AreEqual(16, hash.Length); // MD5 always produces 16 bytes + } + + [TestMethod] + public void ChannelBindings_ComputeHash_Deterministic() + { + var bindings1 = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + var bindings2 = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + + var hash1 = bindings1.ComputeBindingHash(); + var hash2 = bindings2.ComputeBindingHash(); + + Assert.IsTrue(hash1.Span.SequenceEqual(hash2.Span)); + } + + [TestMethod] + public void ChannelBindings_ComputeHash_DifferentData_DifferentHash() + { + var bindings1 = new GssChannelBindings { ApplicationData = new byte[] { 1, 2, 3 } }; + var bindings2 = new GssChannelBindings { ApplicationData = new byte[] { 4, 5, 6 } }; + + var hash1 = bindings1.ComputeBindingHash(); + var hash2 = bindings2.ComputeBindingHash(); + + Assert.IsFalse(hash1.Span.SequenceEqual(hash2.Span)); + } + + [TestMethod] + public void ChannelBindings_ComputeHash_EmptyApplicationData() + { + var bindings = new GssChannelBindings(); + + var hash = bindings.ComputeBindingHash(); + + Assert.AreEqual(16, hash.Length); + } + + [TestMethod] + public void ChannelBindings_ComputeHash_AllFieldsPopulated() + { + var bindings = new GssChannelBindings + { + InitiatorAddrType = 2, + InitiatorAddress = new byte[] { 127, 0, 0, 1 }, + AcceptorAddrType = 2, + AcceptorAddress = new byte[] { 10, 0, 0, 1 }, + ApplicationData = SampleTlsBinding + }; + + var hash = bindings.ComputeBindingHash(); + + Assert.AreEqual(16, hash.Length); + + // Verify it differs from application-data-only version + var bindingsAppOnly = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + var hashAppOnly = bindingsAppOnly.ComputeBindingHash(); + + Assert.IsFalse(hash.Span.SequenceEqual(hashAppOnly.Span)); + } + + // -- DelegationInfo round-trip with channel bindings -- + + [TestMethod] + public void DelegationInfo_ChannelBindings_Roundtrip() + { + var bindings = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + var expectedHash = bindings.ComputeBindingHash(); + + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG, + ChannelBindings = bindings + }; + + var delegInfo = new DelegationInfo(rst); + + Assert.AreEqual(16, delegInfo.ChannelBinding.Length); + Assert.IsTrue(expectedHash.Span.SequenceEqual(delegInfo.ChannelBinding.Span)); + + var encoded = delegInfo.Encode(); + var decoded = new DelegationInfo().Decode(encoded); + + Assert.IsTrue(expectedHash.Span.SequenceEqual(decoded.ChannelBinding.Span)); + } + + [TestMethod] + public void DelegationInfo_NoChannelBindings_ZeroPadded() + { + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG + }; + + var delegInfo = new DelegationInfo(rst); + + // Should be zero-length before encoding + Assert.AreEqual(0, delegInfo.ChannelBinding.Length); + + // After encoding/decoding it becomes 16 zero-bytes + var encoded = delegInfo.Encode(); + var decoded = new DelegationInfo().Decode(encoded); + + Assert.AreEqual(16, decoded.ChannelBinding.Length); + Assert.IsTrue(decoded.ChannelBinding.Span.SequenceEqual(new byte[16])); + } + + // -- Authenticator checksum encoding with channel bindings -- + + [TestMethod] + public void AuthenticatorChecksum_ChannelBindings_InChecksum() + { + var bindings = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG, + ChannelBindings = bindings + }; + + KrbApReq apreq = GenerateApReq(rst, out KrbAuthenticator authenticator); + + Assert.IsNotNull(apreq); + Assert.IsNotNull(authenticator); + Assert.IsNotNull(authenticator.Checksum); + Assert.AreEqual((ChecksumType)0x8003, authenticator.Checksum.Type); + } + + // -- Validation integration tests -- + + [TestMethod] + public void Validate_ChannelBinding_MatchingBindings_Succeeds() + { + var bindings = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG, + ChannelBindings = bindings + }; + + var apReq = GenerateApReqAndDecrypt(rst, out DecryptedKrbApReq decrypted); + + // Set matching expected bindings + decrypted.ExpectedChannelBindings = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + + // Should not throw + decrypted.Validate(ValidationActions.ChannelBinding); + } + + [TestMethod] + [ExpectedException(typeof(KerberosValidationException))] + public void Validate_ChannelBinding_MismatchedBindings_Throws() + { + var bindings = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG, + ChannelBindings = bindings + }; + + var apReq = GenerateApReqAndDecrypt(rst, out DecryptedKrbApReq decrypted); + + // Set different expected bindings + decrypted.ExpectedChannelBindings = new GssChannelBindings + { + ApplicationData = new byte[] { 0xFF, 0xFE, 0xFD, 0xFC } + }; + + // Should throw KerberosValidationException + decrypted.Validate(ValidationActions.ChannelBinding); + } + + [TestMethod] + [ExpectedException(typeof(KerberosValidationException))] + public void Validate_ChannelBinding_AcceptorExpects_InitiatorOmitted_Throws() + { + // Initiator does NOT supply channel bindings + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG + }; + + var apReq = GenerateApReqAndDecrypt(rst, out DecryptedKrbApReq decrypted); + + // Acceptor expects bindings + decrypted.ExpectedChannelBindings = new GssChannelBindings + { + ApplicationData = SampleTlsBinding + }; + + // Should throw because initiator didn't supply bindings but acceptor expects them + decrypted.Validate(ValidationActions.ChannelBinding); + } + + [TestMethod] + public void Validate_ChannelBinding_AcceptorDoesNotExpect_DoesNotThrowIfInitiatorSuppliesBindings() + { + var bindings = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG, + ChannelBindings = bindings + }; + + var apReq = GenerateApReqAndDecrypt(rst, out DecryptedKrbApReq decrypted); + + // Acceptor does NOT set expected bindings (null) + decrypted.ExpectedChannelBindings = null; + + // Should not throw, acceptor doesn't require bindings + decrypted.Validate(ValidationActions.ChannelBinding); + } + + [TestMethod] + public void Validate_ChannelBinding_NotInValidationActions_Skipped() + { + var bindings = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG, + ChannelBindings = bindings + }; + + var apReq = GenerateApReqAndDecrypt(rst, out DecryptedKrbApReq decrypted); + + // Set mismatched bindings but don't include ChannelBinding in validation flags + decrypted.ExpectedChannelBindings = new GssChannelBindings + { + ApplicationData = new byte[] { 0xFF, 0xFE, 0xFD, 0xFC } + }; + + // Should NOT throw — ChannelBinding validation is not requested + decrypted.Validate(ValidationActions.ClientPrincipalIdentifier | ValidationActions.Realm); + } + + [TestMethod] + public void Validate_ChannelBinding_InAllActions_WithMatchingBindings() + { + var bindings = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG, + ChannelBindings = bindings + }; + + var apReq = GenerateApReqAndDecrypt(rst, out DecryptedKrbApReq decrypted); + + decrypted.ExpectedChannelBindings = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + + // Validate with DefaultActions (which includes ChannelBinding via All) + decrypted.Validate(DefaultActions); + } + + [TestMethod] + public void ChannelBindingHash_Extracted_AfterDecrypt() + { + var bindings = new GssChannelBindings { ApplicationData = SampleTlsBinding }; + var expectedHash = bindings.ComputeBindingHash(); + + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG, + ChannelBindings = bindings + }; + + var apReq = GenerateApReqAndDecrypt(rst, out DecryptedKrbApReq decrypted); + + Assert.AreEqual(16, decrypted.ChannelBindingHash.Length); + Assert.IsTrue(expectedHash.Span.SequenceEqual(decrypted.ChannelBindingHash.Span)); + } + + [TestMethod] + public void ChannelBindingHash_AllZeros_WhenNoBindings() + { + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG + }; + + var apReq = GenerateApReqAndDecrypt(rst, out DecryptedKrbApReq decrypted); + + Assert.AreEqual(16, decrypted.ChannelBindingHash.Length); + Assert.IsTrue(decrypted.ChannelBindingHash.Span.SequenceEqual(new byte[16])); + } + + private static readonly KerberosKey ServiceKey = new KerberosKey(key: new byte[16], etype: EncryptionType.AES128_CTS_HMAC_SHA1_96); + + private static KrbApReq GenerateApReq(RequestServiceTicket rst, out KrbAuthenticator authenticator) + { + var key = ServiceKey; + + var now = DateTimeOffset.UtcNow; + var notBefore = now.AddMinutes(-5); + var notAfter = now.AddMinutes(55); + var renewUntil = now.AddMinutes(555); + + var tgsRep = KrbKdcRep.GenerateServiceTicket(new ServiceTicketRequest + { + EncryptedPartKey = key, + Principal = new FakeKerberosPrincipal("test@test.com"), + ServicePrincipal = new FakeKerberosPrincipal("host/test.com"), + ServicePrincipalKey = key, + IncludePac = false, + RealmName = "test.com", + ClientRealmName = "test.com", + Now = now, + StartTime = notBefore, + EndTime = notAfter, + RenewTill = renewUntil, + Flags = TicketFlags.Renewable + }); + + // Extract the session key from the encrypted part + // this is the key the KDC generated inside the ticket + // that the service will use to decrypt the authenticator + var encKdcRepPart = tgsRep.EncPart.Decrypt( + key, + KeyUsage.EncTgsRepPartSessionKey, + d => KrbEncTgsRepPart.DecodeApplication(d) + ); + + var sessionKey = encKdcRepPart.Key.AsKey(); + + return KrbApReq.CreateApReq(tgsRep, sessionKey, rst, out authenticator); + } + + private static KrbApReq GenerateApReqAndDecrypt(RequestServiceTicket rst, out DecryptedKrbApReq decrypted) + { + var apReq = GenerateApReq(rst, out _); + + decrypted = new DecryptedKrbApReq(apReq); + decrypted.Decrypt(ServiceKey); + + return apReq; + } + } +} From b37441a21b55ed7706084ff9196241a78120d7fa Mon Sep 17 00:00:00 2001 From: "Theo Dumitrescu (from Dev Box)" Date: Fri, 20 Feb 2026 07:46:36 -0800 Subject: [PATCH 2/3] out of support for SSPI buffers --- Kerberos.NET/Crypto/DecryptedKrbApReq.cs | 9 ++ .../Entities/GssApi/GssChannelBindings.cs | 52 +++++++ Kerberos.NET/KerberosValidator.cs | 11 ++ .../KrbApReq/ChannelBindingTests.cs | 136 +++++++++++++++++- 4 files changed, 206 insertions(+), 2 deletions(-) diff --git a/Kerberos.NET/Crypto/DecryptedKrbApReq.cs b/Kerberos.NET/Crypto/DecryptedKrbApReq.cs index c199789d..36de27cf 100644 --- a/Kerberos.NET/Crypto/DecryptedKrbApReq.cs +++ b/Kerberos.NET/Crypto/DecryptedKrbApReq.cs @@ -49,6 +49,15 @@ public DecryptedKrbApReq(KrbApReq token, MessageType incomingMessageType = Messa /// public GssChannelBindings ExpectedChannelBindings { get; set; } + /// + /// Convenience property that accepts a raw SEC_CHANNEL_BINDINGS buffer (as returned by Windows SSPI) + /// and converts it to . + /// + public ReadOnlyMemory ExpectedRawChannelBindings + { + set { this.ExpectedChannelBindings = GssChannelBindings.FromSecChannelBindings(value); } + } + public KerberosKey SessionKey { get; private set; } private readonly KrbApReq token; diff --git a/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs b/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs index 39ec0a48..4b3474c4 100644 --- a/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs +++ b/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs @@ -4,6 +4,7 @@ // ----------------------------------------------------------------------- using System; +using System.Buffers.Binary; using System.IO; using System.Security.Cryptography; @@ -14,6 +15,8 @@ namespace Kerberos.NET.Entities /// public class GssChannelBindings { + private const int SecChannelBindingsHeaderSize = 32; + public int InitiatorAddrType { get; set; } public ReadOnlyMemory InitiatorAddress { get; set; } @@ -64,5 +67,54 @@ public ReadOnlyMemory ComputeBindingHash() using var md5 = MD5.Create(); return md5.ComputeHash(data); } + + /// + /// Parses a raw SEC_CHANNEL_BINDINGS flat buffer (as returned by Windows SSPI) into a . + /// + [SupportedOSPlatform("windows")] + public static GssChannelBindings FromSecChannelBindings(ReadOnlyMemory rawBuffer) + { + if (rawBuffer.Length < SecChannelBindingsHeaderSize) + { + throw new ArgumentException( + $"Buffer is too small to contain a SEC_CHANNEL_BINDINGS header. Expected at least {SecChannelBindingsHeaderSize} bytes.", + nameof(rawBuffer)); + } + + var span = rawBuffer.Span; + + var bindings = new GssChannelBindings + { + InitiatorAddrType = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(0)), + }; + + int initiatorLength = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(4)); + int initiatorOffset = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(8)); + + bindings.AcceptorAddrType = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(12)); + + int acceptorLength = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(16)); + int acceptorOffset = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(20)); + + int applicationDataLength = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(24)); + int applicationDataOffset = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(28)); + + if (initiatorLength > 0) + { + bindings.InitiatorAddress = rawBuffer.Slice(initiatorOffset, initiatorLength); + } + + if (acceptorLength > 0) + { + bindings.AcceptorAddress = rawBuffer.Slice(acceptorOffset, acceptorLength); + } + + if (applicationDataLength > 0) + { + bindings.ApplicationData = rawBuffer.Slice(applicationDataOffset, applicationDataLength); + } + + return bindings; + } } } diff --git a/Kerberos.NET/KerberosValidator.cs b/Kerberos.NET/KerberosValidator.cs index f9787318..cdac6c04 100644 --- a/Kerberos.NET/KerberosValidator.cs +++ b/Kerberos.NET/KerberosValidator.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; +using System.Runtime.Versioning; using System.Security; using System.Text; using System.Threading.Tasks; @@ -51,6 +52,16 @@ public KerberosValidator(KeyTable keytab, ILoggerFactory logger = null, ITicketR /// public GssChannelBindings ExpectedChannelBindings { get; set; } + /// + /// Property that accepts a raw SEC_CHANNEL_BINDINGS buffer (as returned by Windows SSPI) + /// and converts it to . + /// + [SupportedOSPlatform("windows")] + public ReadOnlyMemory ExpectedRawChannelBindings + { + set { this.ExpectedChannelBindings = GssChannelBindings.FromSecChannelBindings(value); } + } + private Func nowFunc; public Func Now diff --git a/Tests/Tests.Kerberos.NET/KrbApReq/ChannelBindingTests.cs b/Tests/Tests.Kerberos.NET/KrbApReq/ChannelBindingTests.cs index 97f09b6b..3c611d87 100644 --- a/Tests/Tests.Kerberos.NET/KrbApReq/ChannelBindingTests.cs +++ b/Tests/Tests.Kerberos.NET/KrbApReq/ChannelBindingTests.cs @@ -163,6 +163,138 @@ public void AuthenticatorChecksum_ChannelBindings_InChecksum() Assert.AreEqual((ChecksumType)0x8003, authenticator.Checksum.Type); } + // -- SEC_CHANNEL_BINDINGS parsing tests -- + + [TestMethod] + public void FromSecChannelBindings_ApplicationDataOnly() + { + // Build a SEC_CHANNEL_BINDINGS buffer with only ApplicationData populated + var appData = SampleTlsBinding; + var buffer = BuildSecChannelBindings(0, ReadOnlyMemory.Empty, 0, ReadOnlyMemory.Empty, appData); + + var bindings = GssChannelBindings.FromSecChannelBindings(buffer); + + Assert.AreEqual(0, bindings.InitiatorAddrType); + Assert.AreEqual(0, bindings.InitiatorAddress.Length); + Assert.AreEqual(0, bindings.AcceptorAddrType); + Assert.AreEqual(0, bindings.AcceptorAddress.Length); + Assert.IsTrue(bindings.ApplicationData.Span.SequenceEqual(appData)); + } + + [TestMethod] + public void FromSecChannelBindings_AllFieldsPopulated() + { + var initiator = new byte[] { 127, 0, 0, 1 }; + var acceptor = new byte[] { 10, 0, 0, 1 }; + var appData = SampleTlsBinding; + + var buffer = BuildSecChannelBindings(2, initiator, 2, acceptor, appData); + var bindings = GssChannelBindings.FromSecChannelBindings(buffer); + + Assert.AreEqual(2, bindings.InitiatorAddrType); + Assert.IsTrue(bindings.InitiatorAddress.Span.SequenceEqual(initiator)); + Assert.AreEqual(2, bindings.AcceptorAddrType); + Assert.IsTrue(bindings.AcceptorAddress.Span.SequenceEqual(acceptor)); + Assert.IsTrue(bindings.ApplicationData.Span.SequenceEqual(appData)); + } + + [TestMethod] + public void FromSecChannelBindings_HashMatchesManualConstruction() + { + var appData = SampleTlsBinding; + var buffer = BuildSecChannelBindings(0, ReadOnlyMemory.Empty, 0, ReadOnlyMemory.Empty, appData); + + var fromRaw = GssChannelBindings.FromSecChannelBindings(buffer); + var manual = new GssChannelBindings { ApplicationData = appData }; + + Assert.IsTrue(fromRaw.ComputeBindingHash().Span.SequenceEqual(manual.ComputeBindingHash().Span)); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void FromSecChannelBindings_BufferTooSmall_Throws() + { + GssChannelBindings.FromSecChannelBindings(new byte[16]); + } + + [TestMethod] + public void ExpectedRawChannelBindings_ValidatesCorrectly() + { + var appData = SampleTlsBinding; + var rawBuffer = BuildSecChannelBindings(0, ReadOnlyMemory.Empty, 0, ReadOnlyMemory.Empty, appData); + + var bindings = new GssChannelBindings { ApplicationData = appData }; + + var rst = new RequestServiceTicket + { + GssContextFlags = GssContextEstablishmentFlag.GSS_C_MUTUAL_FLAG, + ChannelBindings = bindings + }; + + var apReq = GenerateApReqAndDecrypt(rst, out DecryptedKrbApReq decrypted); + + // Use the raw buffer convenience property + decrypted.ExpectedRawChannelBindings = rawBuffer; + + // Should not throw — hash matches + decrypted.Validate(ValidationActions.ChannelBinding); + } + + /// + /// Builds a SEC_CHANNEL_BINDINGS flat buffer in the Windows SSPI layout. + /// + private static byte[] BuildSecChannelBindings( + int initiatorAddrType, ReadOnlyMemory initiatorAddress, + int acceptorAddrType, ReadOnlyMemory acceptorAddress, + ReadOnlyMemory applicationData) + { + const int headerSize = 32; + int offset = headerSize; + + int initiatorOffset = initiatorAddress.Length > 0 ? offset : 0; + offset += initiatorAddress.Length; + + int acceptorOffset = acceptorAddress.Length > 0 ? offset : 0; + offset += acceptorAddress.Length; + + int appDataOffset = applicationData.Length > 0 ? offset : 0; + offset += applicationData.Length; + + var buffer = new byte[offset]; + + using (var ms = new System.IO.MemoryStream(buffer)) + using (var writer = new System.IO.BinaryWriter(ms)) + { + writer.Write(initiatorAddrType); + writer.Write(initiatorAddress.Length); + writer.Write(initiatorOffset); + + writer.Write(acceptorAddrType); + writer.Write(acceptorAddress.Length); + writer.Write(acceptorOffset); + + writer.Write(applicationData.Length); + writer.Write(appDataOffset); + + if (initiatorAddress.Length > 0) + { + writer.Write(initiatorAddress.ToArray()); + } + + if (acceptorAddress.Length > 0) + { + writer.Write(acceptorAddress.ToArray()); + } + + if (applicationData.Length > 0) + { + writer.Write(applicationData.ToArray()); + } + } + + return buffer; + } + // -- Validation integration tests -- [TestMethod] @@ -270,7 +402,7 @@ public void Validate_ChannelBinding_NotInValidationActions_Skipped() ApplicationData = new byte[] { 0xFF, 0xFE, 0xFD, 0xFC } }; - // Should NOT throw — ChannelBinding validation is not requested + // Should NOT throw, ChannelBinding validation is not requested decrypted.Validate(ValidationActions.ClientPrincipalIdentifier | ValidationActions.Realm); } @@ -289,7 +421,7 @@ public void Validate_ChannelBinding_InAllActions_WithMatchingBindings() decrypted.ExpectedChannelBindings = new GssChannelBindings { ApplicationData = SampleTlsBinding }; - // Validate with DefaultActions (which includes ChannelBinding via All) + // Validate with DefaultActions, which includes ChannelBinding decrypted.Validate(DefaultActions); } From 7302181744f980a7a07ec813c5b4e7aec504fd25 Mon Sep 17 00:00:00 2001 From: "Theo Dumitrescu (from Dev Box)" Date: Fri, 20 Feb 2026 09:31:42 -0800 Subject: [PATCH 3/3] Add GSS channel binding (CBT) support for TGS-REQ validation --- .../Entities/GssApi/GssChannelBindings.cs | 1 - Kerberos.NET/Entities/Krb/KrbTgsReq.cs | 11 +- Kerberos.NET/KerberosValidator.cs | 1 - Kerberos.NET/Server/KdcMessageHandlerBase.cs | 24 ++- Kerberos.NET/Server/PaDataTgsTicketHandler.cs | 8 +- .../Server/PreAuthenticationContext.cs | 5 + .../Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs | 174 ++++++++++++++++++ 7 files changed, 215 insertions(+), 9 deletions(-) diff --git a/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs b/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs index 4b3474c4..2d3996b4 100644 --- a/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs +++ b/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs @@ -71,7 +71,6 @@ public ReadOnlyMemory ComputeBindingHash() /// /// Parses a raw SEC_CHANNEL_BINDINGS flat buffer (as returned by Windows SSPI) into a . /// - [SupportedOSPlatform("windows")] public static GssChannelBindings FromSecChannelBindings(ReadOnlyMemory rawBuffer) { if (rawBuffer.Length < SecChannelBindingsHeaderSize) diff --git a/Kerberos.NET/Entities/Krb/KrbTgsReq.cs b/Kerberos.NET/Entities/Krb/KrbTgsReq.cs index cefeb573..ac06d1d4 100644 --- a/Kerberos.NET/Entities/Krb/KrbTgsReq.cs +++ b/Kerberos.NET/Entities/Krb/KrbTgsReq.cs @@ -113,7 +113,7 @@ out KrbEncryptionKey sessionKey KeyUsage.PaTgsReqChecksum ); - var tgtApReq = CreateApReq(kdcRep, tgtSessionKey, bodyChecksum, out sessionKey); + var tgtApReq = CreateApReq(kdcRep, tgtSessionKey, bodyChecksum, rst.ChannelBindings, out sessionKey); var pacOptions = new KrbPaPacOptions { @@ -200,7 +200,7 @@ private static ReadOnlyMemory EncodeS4URequest(string s4u, X509Certificate return paX509.Encode(); } - private static KrbApReq CreateApReq(KrbKdcRep kdcRep, KrbEncryptionKey tgtSessionKey, KrbChecksum checksum, out KrbEncryptionKey sessionKey) + private static KrbApReq CreateApReq(KrbKdcRep kdcRep, KrbEncryptionKey tgtSessionKey, KrbChecksum checksum, GssChannelBindings channelBindings, out KrbEncryptionKey sessionKey) { var tgt = kdcRep.Ticket; @@ -212,6 +212,13 @@ private static KrbApReq CreateApReq(KrbKdcRep kdcRep, KrbEncryptionKey tgtSessio Checksum = checksum }; + if (channelBindings != null) + { + var delegInfo = new DelegationInfo(); + delegInfo.ChannelBinding = channelBindings.ComputeBindingHash(); + authenticator.Checksum = KrbChecksum.EncodeDelegationChecksum(delegInfo); + } + sessionKey = KrbEncryptionKey.Generate(tgtSessionKey.EType); sessionKey.Usage = KeyUsage.EncTgsRepPartSubSessionKey; diff --git a/Kerberos.NET/KerberosValidator.cs b/Kerberos.NET/KerberosValidator.cs index cdac6c04..c9ba31f5 100644 --- a/Kerberos.NET/KerberosValidator.cs +++ b/Kerberos.NET/KerberosValidator.cs @@ -56,7 +56,6 @@ public KerberosValidator(KeyTable keytab, ILoggerFactory logger = null, ITicketR /// Property that accepts a raw SEC_CHANNEL_BINDINGS buffer (as returned by Windows SSPI) /// and converts it to . /// - [SupportedOSPlatform("windows")] public ReadOnlyMemory ExpectedRawChannelBindings { set { this.ExpectedChannelBindings = GssChannelBindings.FromSecChannelBindings(value); } diff --git a/Kerberos.NET/Server/KdcMessageHandlerBase.cs b/Kerberos.NET/Server/KdcMessageHandlerBase.cs index 6c428aa8..8d74c0f7 100644 --- a/Kerberos.NET/Server/KdcMessageHandlerBase.cs +++ b/Kerberos.NET/Server/KdcMessageHandlerBase.cs @@ -25,6 +25,20 @@ public abstract class KdcMessageHandlerBase protected KdcServerOptions Options { get; } + /// + /// Expected channel bindings for this request's TGS-REQ validation. + /// + public GssChannelBindings ExpectedChannelBindings { get; set; } + + /// + /// Accepts a raw SEC_CHANNEL_BINDINGS buffer + /// and converts it to . + /// + public ReadOnlyMemory ExpectedRawChannelBindings + { + set { this.ExpectedChannelBindings = GssChannelBindings.FromSecChannelBindings(value); } + } + protected IRealmService RealmService { get; private set; } public IDictionary PreAuthHandlers => this.preAuthHandlers; @@ -103,7 +117,10 @@ public virtual async Task> ExecuteAsync() { try { - var context = new PreAuthenticationContext(); + var context = new PreAuthenticationContext + { + ExpectedChannelBindings = this.ExpectedChannelBindings + }; this.DecodeMessage(context); @@ -127,7 +144,10 @@ public virtual ReadOnlyMemory Execute() { try { - var context = new PreAuthenticationContext(); + var context = new PreAuthenticationContext + { + ExpectedChannelBindings = this.ExpectedChannelBindings + }; this.DecodeMessage(context); diff --git a/Kerberos.NET/Server/PaDataTgsTicketHandler.cs b/Kerberos.NET/Server/PaDataTgsTicketHandler.cs index 1e1a41f8..a67712c9 100644 --- a/Kerberos.NET/Server/PaDataTgsTicketHandler.cs +++ b/Kerberos.NET/Server/PaDataTgsTicketHandler.cs @@ -39,7 +39,7 @@ public override void PreValidate(PreAuthenticationContext preauth) var state = preauth.GetState(PaDataType.PA_TGS_REQ); - state.DecryptedApReq = this.DecryptApReq(state.ApReq, preauth.EvidenceTicketKey); + state.DecryptedApReq = this.DecryptApReq(state.ApReq, preauth.EvidenceTicketKey, preauth.ExpectedChannelBindings); } /// @@ -101,7 +101,7 @@ public override KrbPaData Validate(KrbKdcReq asReq, PreAuthenticationContext con var state = context.GetState(PaDataType.PA_TGS_REQ); - state.DecryptedApReq ??= this.DecryptApReq(state.ApReq, context.EvidenceTicketKey); + state.DecryptedApReq ??= this.DecryptApReq(state.ApReq, context.EvidenceTicketKey, context.ExpectedChannelBindings); context.EncryptedPartKey = state.DecryptedApReq.SessionKey; context.Ticket = state.DecryptedApReq.Ticket; @@ -135,12 +135,14 @@ public static KrbApReq ExtractApReq(PreAuthenticationContext context) return state.ApReq; } - private DecryptedKrbApReq DecryptApReq(KrbApReq apReq, KerberosKey krbtgtKey) + private DecryptedKrbApReq DecryptApReq(KrbApReq apReq, KerberosKey krbtgtKey, GssChannelBindings expectedChannelBindings) { var apReqDecrypted = new DecryptedKrbApReq(apReq, MessageType.KRB_TGS_REQ); apReqDecrypted.Decrypt(krbtgtKey); + apReqDecrypted.ExpectedChannelBindings = expectedChannelBindings; + apReqDecrypted.Validate(this.Validation); return apReqDecrypted; diff --git a/Kerberos.NET/Server/PreAuthenticationContext.cs b/Kerberos.NET/Server/PreAuthenticationContext.cs index c729f03d..87659b59 100644 --- a/Kerberos.NET/Server/PreAuthenticationContext.cs +++ b/Kerberos.NET/Server/PreAuthenticationContext.cs @@ -90,6 +90,11 @@ public class PreAuthenticationContext /// public bool? IncludePac { get; set; } + /// + /// Expected channel bindings for AP-REQ validation during TGS-REQ processing. + /// + public GssChannelBindings ExpectedChannelBindings { get; set; } + /// /// Retrieve the current pre-authentication state for a particular PA-Data type. /// If the initial state is not present it will be created. diff --git a/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs b/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs index 939858bf..b695ebcf 100644 --- a/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs +++ b/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs @@ -495,5 +495,179 @@ public void AsReqPreAuth_PkinitCertificateAccessible() Assert.AreEqual(credCert.Thumbprint, clientCert.Thumbprint); } } + + // -- TGS-REQ Channel Binding Tests -- + + private static readonly byte[] TgsTestChannelBinding = new byte[] + { + 0x74, 0x6C, 0x73, 0x2D, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x2D, 0x65, 0x6E, 0x64, 0x2D, 0x70, + 0x6F, 0x69, 0x6E, 0x74, 0x3A, 0xAA, 0xBB, 0xCC, + 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44 + }; + + [TestMethod] + public void KdcTgsReq_ChannelBinding_MatchingBindings_Succeeds() + { + // Channel bindings used by client in TGS-REQ + // The same channel bindings are expected by the server + // thus should result in a successful TGS-REQ processing + var bindings = new GssChannelBindings { ApplicationData = TgsTestChannelBinding }; + + KrbAsRep asRep = RequestTgt(cname: Upn, crealm: Realm, srealm: Realm, out KrbEncryptionKey tgtKey); + + var tgsReq = KrbTgsReq.CreateTgsReq( + new RequestServiceTicket + { + Realm = Realm, + ServicePrincipalName = "host/foo." + Realm, + ChannelBindings = bindings + }, + tgtKey, asRep, out _); + + var handler = new KdcTgsReqMessageHandler(tgsReq.EncodeApplication(), new KdcServerOptions + { + DefaultRealm = Realm, + IsDebug = true, + RealmLocator = realm => new FakeRealmService(realm) + }); + + + handler.ExpectedChannelBindings = bindings; + + var results = handler.Execute(); + + var tgsRep = KrbTgsRep.DecodeApplication(results); + Assert.IsNotNull(tgsRep); + } + + [TestMethod] + public void KdcTgsReq_ChannelBinding_Mismatch_ReturnsError() + { + // Channel bindings used by client in TGS-REQ + // Different channel bindings are expected by the server + // thus should result in an error + var clientBindings = new GssChannelBindings { ApplicationData = TgsTestChannelBinding }; + var serverBindings = new GssChannelBindings { ApplicationData = new byte[] { 0xFF, 0xFE, 0xFD } }; + + KrbAsRep asRep = RequestTgt(cname: Upn, crealm: Realm, srealm: Realm, out KrbEncryptionKey tgtKey); + + var tgsReq = KrbTgsReq.CreateTgsReq( + new RequestServiceTicket + { + Realm = Realm, + ServicePrincipalName = "host/foo." + Realm, + ChannelBindings = clientBindings + }, + tgtKey, asRep, out _); + + var handler = new KdcTgsReqMessageHandler(tgsReq.EncodeApplication(), new KdcServerOptions + { + DefaultRealm = Realm, + IsDebug = true, + RealmLocator = realm => new FakeRealmService(realm) + }); + + handler.ExpectedChannelBindings = serverBindings; + + var results = handler.Execute(); + + var error = KrbError.DecodeApplication(results); + Assert.AreEqual(KerberosErrorCode.KRB_ERR_GENERIC, error.ErrorCode); + } + + [TestMethod] + public void KdcTgsReq_ChannelBinding_ServerExpectsNone_Succeeds() + { + // Channel bindings used by client in TGS-REQ + var clientBindings = new GssChannelBindings { ApplicationData = TgsTestChannelBinding }; + + KrbAsRep asRep = RequestTgt(cname: Upn, crealm: Realm, srealm: Realm, out KrbEncryptionKey tgtKey); + + var tgsReq = KrbTgsReq.CreateTgsReq( + new RequestServiceTicket + { + Realm = Realm, + ServicePrincipalName = "host/foo." + Realm, + ChannelBindings = clientBindings + }, + tgtKey, asRep, out _); + + // Server does not expect channel bindings + var handler = new KdcTgsReqMessageHandler(tgsReq.EncodeApplication(), new KdcServerOptions + { + DefaultRealm = Realm, + IsDebug = true, + RealmLocator = realm => new FakeRealmService(realm) + // ExpectedChannelBindings = null + }); + + var results = handler.Execute(); + + // Should succeed even though client included channel bindings as the server does not require them + var tgsRep = KrbTgsRep.DecodeApplication(results); + Assert.IsNotNull(tgsRep); + } + + [TestMethod] + public void KdcTgsReq_ChannelBinding_ServerExpects_ClientOmits_ReturnsError() + { + // Server expects channel bindings but client omits them in TGS-REQ + var serverBindings = new GssChannelBindings { ApplicationData = TgsTestChannelBinding }; + + KrbAsRep asRep = RequestTgt(cname: Upn, crealm: Realm, srealm: Realm, out KrbEncryptionKey tgtKey); + + var tgsReq = KrbTgsReq.CreateTgsReq( + new RequestServiceTicket + { + Realm = Realm, + ServicePrincipalName = "host/foo." + Realm + }, + tgtKey, asRep, out _); + + // Server expects channel bindings + var handler = new KdcTgsReqMessageHandler(tgsReq.EncodeApplication(), new KdcServerOptions + { + DefaultRealm = Realm, + IsDebug = true, + RealmLocator = realm => new FakeRealmService(realm) + }); + + handler.ExpectedChannelBindings = serverBindings; + + var results = handler.Execute(); + + // Expect an error due to missing channel bindings in client + var error = KrbError.DecodeApplication(results); + Assert.AreEqual(KerberosErrorCode.KRB_ERR_GENERIC, error.ErrorCode); + } + + [TestMethod] + public void KdcTgsReq_NoChannelBindings_Succeeds() + { + // Neither client nor server uses channel bindings + // should succeed without error + KrbAsRep asRep = RequestTgt(cname: Upn, crealm: Realm, srealm: Realm, out KrbEncryptionKey tgtKey); + + var tgsReq = KrbTgsReq.CreateTgsReq( + new RequestServiceTicket + { + Realm = Realm, + ServicePrincipalName = "host/foo." + Realm + }, + tgtKey, asRep, out _); + + var handler = new KdcTgsReqMessageHandler(tgsReq.EncodeApplication(), new KdcServerOptions + { + DefaultRealm = Realm, + IsDebug = true, + RealmLocator = realm => new FakeRealmService(realm) + }); + + var results = handler.Execute(); + + var tgsRep = KrbTgsRep.DecodeApplication(results); + Assert.IsNotNull(tgsRep); + } } }