diff --git a/Kerberos.NET/Crypto/DecryptedKrbApReq.cs b/Kerberos.NET/Crypto/DecryptedKrbApReq.cs
index 45682a71..36de27cf 100644
--- a/Kerberos.NET/Crypto/DecryptedKrbApReq.cs
+++ b/Kerberos.NET/Crypto/DecryptedKrbApReq.cs
@@ -36,6 +36,28 @@ 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; }
+
+ ///
+ /// 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;
@@ -161,6 +183,8 @@ private KrbEncKrbCredPart TryExtractDelegationTicket(KrbChecksum checksum)
var delegationInfo = checksum.DecodeDelegation();
+ this.ChannelBindingHash = delegationInfo.ChannelBinding;
+
var delegation = delegationInfo?.DelegationTicket;
if (delegation == null)
@@ -212,6 +236,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..2d3996b4
--- /dev/null
+++ b/Kerberos.NET/Entities/GssApi/GssChannelBindings.cs
@@ -0,0 +1,119 @@
+// -----------------------------------------------------------------------
+// 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.Buffers.Binary;
+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
+ {
+ private const int SecChannelBindingsHeaderSize = 32;
+
+ 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);
+ }
+
+ ///
+ /// Parses a raw SEC_CHANNEL_BINDINGS flat buffer (as returned by Windows SSPI) into a .
+ ///
+ 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/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/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/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..c9ba31f5 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;
@@ -46,6 +47,20 @@ 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; }
+
+ ///
+ /// 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); }
+ }
+
private Func nowFunc;
public Func Now
@@ -83,6 +98,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/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/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/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);
+ }
}
}
diff --git a/Tests/Tests.Kerberos.NET/KrbApReq/ChannelBindingTests.cs b/Tests/Tests.Kerberos.NET/KrbApReq/ChannelBindingTests.cs
new file mode 100644
index 00000000..3c611d87
--- /dev/null
+++ b/Tests/Tests.Kerberos.NET/KrbApReq/ChannelBindingTests.cs
@@ -0,0 +1,511 @@
+// -----------------------------------------------------------------------
+// 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);
+ }
+
+ // -- 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]
+ 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
+ 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;
+ }
+ }
+}