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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions Kerberos.NET/Crypto/DecryptedKrbApReq.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ public DecryptedKrbApReq(KrbApReq token, MessageType incomingMessageType = Messa

public KrbEncKrbCredPart DelegationTicket { get; private set; }

/// <summary>
/// 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
/// <see cref="GssChannelBindings"/> structure. Will be null or empty if no channel
/// bindings were supplied by the initiator.
/// </summary>
public ReadOnlyMemory<byte> ChannelBindingHash { get; private set; }

/// <summary>
/// Expected channel bindings to validate against when <see cref="ValidationActions.ChannelBinding"/> is enabled.
/// </summary>
public GssChannelBindings ExpectedChannelBindings { get; set; }

/// <summary>
/// Convenience property that accepts a raw SEC_CHANNEL_BINDINGS buffer (as returned by Windows SSPI)
/// and converts it to <see cref="ExpectedChannelBindings"/>.
/// </summary>
public ReadOnlyMemory<byte> ExpectedRawChannelBindings
{
set { this.ExpectedChannelBindings = GssChannelBindings.FromSecChannelBindings(value); }
}

public KerberosKey SessionKey { get; private set; }

private readonly KrbApReq token;
Expand Down Expand Up @@ -161,6 +183,8 @@ private KrbEncKrbCredPart TryExtractDelegationTicket(KrbChecksum checksum)

var delegationInfo = checksum.DecodeDelegation();

this.ChannelBindingHash = delegationInfo.ChannelBinding;

var delegation = delegationInfo?.DelegationTicket;

if (delegation == null)
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

If you passed ValidationActions.ChannelBinding but forgot to pass ExpectedChannelBindings -- should this be an exception? Or maybe it's not a wise idea -- but just trying to see if there's a way to not fail silently

Copy link
Contributor

Choose a reason for hiding this comment

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

Or maybe you could make ExpectedChannelBindings a mandatory param of Validate?

{
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()
Expand Down
119 changes: 119 additions & 0 deletions Kerberos.NET/Entities/GssApi/GssChannelBindings.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents gss_channel_bindings_struct per RFC 4121 section 4.1.1.2.
/// </summary>
public class GssChannelBindings
{
private const int SecChannelBindingsHeaderSize = 32;

public int InitiatorAddrType { get; set; }

public ReadOnlyMemory<byte> InitiatorAddress { get; set; }

public int AcceptorAddrType { get; set; }

public ReadOnlyMemory<byte> AcceptorAddress { get; set; }

/// <summary>
/// Protocol-specific channel binding data
/// e.g. tls-server-end-point or tls-unique as per RFC 5929
/// </summary>
public ReadOnlyMemory<byte> ApplicationData { get; set; }

/// <summary>
/// Computes the 16-byte MD5 binding hash (Bnd field) per RFC 4121 section 4.1.1.2.
/// </summary>
public ReadOnlyMemory<byte> 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);
}

/// <summary>
/// Parses a raw SEC_CHANNEL_BINDINGS flat buffer (as returned by Windows SSPI) into a <see cref="GssChannelBindings"/>.
/// </summary>
public static GssChannelBindings FromSecChannelBindings(ReadOnlyMemory<byte> 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;
}
}
}
5 changes: 5 additions & 0 deletions Kerberos.NET/Entities/Krb/DelegationInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> Encode()
Expand Down
11 changes: 9 additions & 2 deletions Kerberos.NET/Entities/Krb/KrbTgsReq.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -200,7 +200,7 @@ private static ReadOnlyMemory<byte> 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;

Expand All @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions Kerberos.NET/Entities/RequestServiceTicket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public struct RequestServiceTicket : IEquatable<RequestServiceTicket>
/// </summary>
public GssContextEstablishmentFlag GssContextFlags { get; set; }

/// <summary>
/// Optional GSS channel bindings to include in the authenticator checksum.
/// </summary>
public GssChannelBindings ChannelBindings { get; set; }

/// <summary>
/// Includes additional configuration details for the request.
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions Kerberos.NET/KerberosValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System;
using System.Globalization;
using System.Runtime.Versioning;
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably not needed?

using System.Security;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -46,6 +47,20 @@ public KerberosValidator(KeyTable keytab, ILoggerFactory logger = null, ITicketR

public ValidationActions ValidateAfterDecrypt { get; set; }

/// <summary>
/// Expected channel bindings to validate during decryption.
/// </summary>
public GssChannelBindings ExpectedChannelBindings { get; set; }

/// <summary>
/// Property that accepts a raw SEC_CHANNEL_BINDINGS buffer (as returned by Windows SSPI)
/// and converts it to <see cref="ExpectedChannelBindings"/>.
/// </summary>
public ReadOnlyMemory<byte> ExpectedRawChannelBindings
{
set { this.ExpectedChannelBindings = GssChannelBindings.FromSecChannelBindings(value); }
}

private Func<DateTimeOffset> nowFunc;

public Func<DateTimeOffset> Now
Expand Down Expand Up @@ -83,6 +98,7 @@ public async Task<DecryptedKrbApReq> Validate(ReadOnlyMemory<byte> requestBytes)
this.logger.LogTrace("Kerberos request decrypted {SName}", decryptedToken.SName.FullyQualifiedName);

decryptedToken.Now = this.Now;
decryptedToken.ExpectedChannelBindings = this.ExpectedChannelBindings;

if (this.ValidateAfterDecrypt > 0)
{
Expand Down
24 changes: 22 additions & 2 deletions Kerberos.NET/Server/KdcMessageHandlerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ public abstract class KdcMessageHandlerBase

protected KdcServerOptions Options { get; }

/// <summary>
/// Expected channel bindings for this request's TGS-REQ validation.
/// </summary>
public GssChannelBindings ExpectedChannelBindings { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe makes sense to just store the hash, instead of the full object? Seems like you just need the hash


/// <summary>
/// Accepts a raw SEC_CHANNEL_BINDINGS buffer
/// and converts it to <see cref="ExpectedChannelBindings"/>.
/// </summary>
public ReadOnlyMemory<byte> ExpectedRawChannelBindings
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I feel like intention might be clearer with a setter method instead of a setter-only field. Or you could just make the caller call GssChannelBindings.FromSecChannelBindings. But no strong opinion on this.

{
set { this.ExpectedChannelBindings = GssChannelBindings.FromSecChannelBindings(value); }
}

protected IRealmService RealmService { get; private set; }

public IDictionary<PaDataType, PreAuthHandlerConstructor> PreAuthHandlers => this.preAuthHandlers;
Expand Down Expand Up @@ -103,7 +117,10 @@ public virtual async Task<ReadOnlyMemory<byte>> ExecuteAsync()
{
try
{
var context = new PreAuthenticationContext();
var context = new PreAuthenticationContext
{
ExpectedChannelBindings = this.ExpectedChannelBindings
};

this.DecodeMessage(context);

Expand All @@ -127,7 +144,10 @@ public virtual ReadOnlyMemory<byte> Execute()
{
try
{
var context = new PreAuthenticationContext();
var context = new PreAuthenticationContext
{
ExpectedChannelBindings = this.ExpectedChannelBindings
};

this.DecodeMessage(context);

Expand Down
8 changes: 5 additions & 3 deletions Kerberos.NET/Server/PaDataTgsTicketHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public override void PreValidate(PreAuthenticationContext preauth)

var state = preauth.GetState<TgsState>(PaDataType.PA_TGS_REQ);

state.DecryptedApReq = this.DecryptApReq(state.ApReq, preauth.EvidenceTicketKey);
state.DecryptedApReq = this.DecryptApReq(state.ApReq, preauth.EvidenceTicketKey, preauth.ExpectedChannelBindings);
}

/// <summary>
Expand Down Expand Up @@ -101,7 +101,7 @@ public override KrbPaData Validate(KrbKdcReq asReq, PreAuthenticationContext con

var state = context.GetState<TgsState>(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;
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions Kerberos.NET/Server/PreAuthenticationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ public class PreAuthenticationContext
/// </summary>
public bool? IncludePac { get; set; }

/// <summary>
/// Expected channel bindings for AP-REQ validation during TGS-REQ processing.
/// </summary>
public GssChannelBindings ExpectedChannelBindings { get; set; }

/// <summary>
/// Retrieve the current pre-authentication state for a particular PA-Data type.
/// If the initial state is not present it will be created.
Expand Down
7 changes: 6 additions & 1 deletion Kerberos.NET/ValidationAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,14 @@ public enum ValidationActions
/// </summary>
SequenceNumberGreaterThan = 1 << 9,

/// <summary>
/// Validates channel bindings in the authenticator checksum.
/// </summary>
ChannelBinding = 1 << 10,

/// <summary>
/// Indicates all validation actions must be invoked.
/// </summary>
All = ClientPrincipalIdentifier | Realm | TokenWindow | StartTime | EndTime | Replay | Pac | RenewTill
All = ClientPrincipalIdentifier | Realm | TokenWindow | StartTime | EndTime | Replay | Pac | RenewTill | ChannelBinding
}
}
Loading